1장 C++에 왔으면 C++의 법을 따릅시다 🏳
Effective C++ 책을 읽고 정리하고자 합니다.
해당 포스팅은 이전 포스팅에 연계되어 진행됩니다.
이전 포스팅 을 읽고 해당 포스팅을 읽으면 더욱 더 이해하기 쉬울 겁니다 🙏
궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇
const 함수 선언
const의 가장 강점은 함수 선언 할 때 사용하는 것이다. 함수 선언문에 있어 const 는 “반환값” (이전 포스팅 에서 다룸.), “각각의 매개변수”, “멤버함수” 앞 혹은 함수 전체에 const 성질을 부여할 수도 있다.
🌱 매개변수의 const 화
보통 매개변수를 const
화 하는 이유는 “상수 객체”를 사용하자는 것이다. 코드
효율을 위해 아주 중요한 부분이기도 하다. 우리가 이후 항목 20 에서 다루겠지만 C++ 프로그램의 성능을 높이는 핵심 기법 중 하나로 객체 전달을 상수 객체에 대한 참조 (reference-to-const) 기법을 사용한다.
실제 프로그램 내에서 상수객체가 생기는 경우는 두 가지이다.
- 상수 객체에 대한 포인터가 전달될 경우
- 상수 객체에 대한 참조자로 객체가 전달 될 경우
// 해당 함수는 상수 객체에 대한 참조자로 전달 될 경우다.
void print (const TextBlock& ctb) {
...
}
위의 예시를 보면 객체를 print
함수에 전달 할 경우 상수 객체가 생긴다는 것을 알
수 있다. 참조자는 포인터로 구현이 가능하다. (해당 부분은 항목 20부분에서
다루도록 하자.)
🌱 멤버 함수의 const 화
멤버 함수에 붙은 const
키워드의 역할은 “해당 멤버 함수가 상수 객체에 대해 호출
될 함수이다.” 라는 것을 알려 주는 것이다.
왜 중요할까? 🤔
1️⃣ 첫째는 클래스의 인터페이스를 이해하기 좋게 하기 위해서이다.
- 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고, 변경할 수 없는 함수는 무엇인지 한 눈에 알아볼 수 있다.
2️⃣ 둘째는 상수 객체를 사용할 수 있는 환경을 구축할 수 있다.
- 위에서 언급한 상수 객체에 대한 참조 (reference-to-const)를 제대로 살아 움직이게 하기 위해서는 상수 상태로 전달된 객체를 조작할 수 있는
const
멤버 함수가, 즉 상수 멤버 함수가 존재해야한다.
두 번째 내용을 조금 더 자세히 알아보자.
const
키워드 유무에 따라 멤버함수의 오버로딩이 가능하다.
class TextBox {
pulbic:
...
const char& operator[](std::size_t position) const // 상수 객체에 대한
{ return text[position]; } // operator[]
const char& operator[](std::size_t position) // 비상수 객체에 대한
{ return text[position]; } // operator[]
private:
std::string text;
}
위의 예시에서 보듯 멤버함수를 상수 객체로 만들기 위해서는 const
키워드를 함수선언 마지막 부분에 붙여야한다. 해당 선언은 다음과 같이 사용할 수 있다.
TextBlock tb("Hello");
std::cout << tb[0]; // 비상수 객체에 대한 operator[] 호출
const TextBlock ctb("World");
std::cout << ctb[0]; // 상수 객체에 대한 operator[] 호출
일반 Textblock객체를 만들어 사용하게 될 경우 const
로 선언되지 않은 비상수 객체 가 호출 된다. 만약 const
기반의 객체를 생성한 후 operator[]
를 호출할 경우 const가 선언된 상수 객체 가 호출 된다. 여기서 알 수 있는 점은 const
라는 키워드를 멤버 함수 끝에 붙일 경우 내부의 모든 것은 const
로 인식하게 된다는 점이다. 즉, 전달 받는 객체에 const
가 적혀 있지 않아도 const
로 인식되고 멤버함수 내에 어떤 값도 변경되면 안된다는 것을 의미한다. 해당 개념을 비트수준 상수성 이라고 한다. 아래의 상수성 내용에서 자세히 다루겠다.
overload에 대해 한 가지 주의할 점을 알아보자.
const TextBlock ctb("Hello");
std::cout << ctb[0]; // 문제 없이 동작함.
ctb[0] = 'x'; // 에러 발생
해당 에러는 const
멤버함수를 부르면서 발생한 에러가 아니라 operator[]
의 반환 타입 const char&
로 인해 발생한다는 점을 기억해야한다. 즉, operator[]
를 호출했을 때 정상동작하고 반환 값에 새로운 값을 넣으려고 시도해서 발생하는 에러라는 사실을 꼭 기억해야한다. (const
를 썻다고 에러나구나 하고 지우면 안되고 원인을 분석해봐야한다..)
여기서 눈여겨 봐야할 점은 operator[]
가 const char or char
타입만 반환하는 것이 아닌 참조 값 (reference) 를 반환한다는 점이다. 해당 부분은 잘 기억해야하는데, C++의 기본적으로 값을 적으면 “값에의한 반환”을 시도한다. 따라서, 원본 값이 아닌 사본 값이 반환 되게 된다. 사본 값에 의해 값을 변경하게 될 경우 나중에 버그를 찾느라 하루종일을 보낼수도 있다는 점을 기억하자.
상수성
상수성에서는 물리적 상수성 (비트수준 상수성) 과 논리적 상수성 두 가지 개념으로 나뉜다.
🌱 물리적 상수성 (이하. 비트수준 상수성)
멤버함수의 비트수준 상수성은 어떤 멤버함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 함수는 const
하다라는 것을 인정하는 개념이다. 즉, 멤버함수 내 어떤 데이터도 변경하면 안된다는 의미이다. 우리가 멤버함수를 const
화 한다는 개념은 비트수준 상수성을 취하겠다는 의미이다. 비트수준 상수성을 사용하면 상수성 위반을 검사하는데 시간이 오래 걸리지 않는다. 컴파일러는 해당 멤버함수 내에서 = (대입 연산자)
가 사용됐는지만 확인하면 되기 때문이다.
컴파일러 단에서 =
을 확인해 const
를 보장해줄 것 같지만, “const의 역할”을 하지 못하는 멤버함수들도 비트수준 상수성을 통과해 const
를 보장한다고 주장하는 문제가있다.
class CTextBlock {
public:
...
char& operator[](std::size_t position) const
{
return pText[position]; // 비트수준 상수성 통과
// const화 해주겠다라는 의미.
}
private:
char *pText;
};
위의 코드를 보자. 해당 코드에서는 C와 언어 호환을 위해 string객체가 아닌 char * (pointer)
로 Text 자료구조를 관리 한다. opeartor[]
내부를 보면, 실제 변경되는 건 없기 때문에 비트수준 상수성 검사는 충분히 통과하고 const
화 됐다고 볼 수 있다. 하지만, 아래와 같이 opeartor 후 값을 변경하면 어떻게 될까?
const CTextBlock cctb("Hello"); // 상수 객체 선언
char *pc = &cctb[0]; // 상수 버전의 opeartor[]호출 후
// 내부 데이터의 포인터 획득
*pc = 'J'; // 값 변경으로 "Jello"가 됨
위의 코드를 보면 확실히 무언가 잘못됨을 감지할 것이다. const
화 했다고 이야기 했는데!! 값이 변경되는 기이한 현상을 만나게 되다니.. 비트수준 상수성을 취할 경우 위와 같은 문제를 만날 수 있다. 해당 문제를 해결하기 위해 등장한 개념이 논리적 상수성 이다.
🌱 논리적 상수성
멤버함수의 논리적 상수성이란 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자 측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다라는 의미이다. 아래의 문장 길이를 구하는 예시를 보자. 해당 함수는 상수멤버함수 이고, 문장의 길이를 caching 해두어 매번 strlen()
를 사용하는 부하를 줄이기 위해 최적화한 함수이다.
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
std::size_t textLength; // 바로 직전에 계산한 텍스트 길이
bool lengthIsValid; // 현재 길이가 유효한가?
};
std::size_t CTextBlock::length() const {
if (!lengthIsValid) {
textLength = std::strlen(pText); // 에러! 상수 멤버 함수 내 = 은 사용금지
lengthIsValid = true; // 에러!
}
return textLength;
}
해당 예시는 비트수준 상수성 기준으로 볼 때, =
연산자가 들어가는 caching 부분으로 인해 에러가 발생하게 된다. 당연히 textLength
와 lengthIsValid
같은 경우는 값이 변경되야하기 때문에 비트수준 상수성에는 부합되지 않는다. 하지만, 개발자 입장에서 볼 땐, 최적화를 위해선 당연히 필요한 부분이고, length 라는 함수에 영향을 끼치지도, 그렇다고 길이를 바꾸지도 않으니 사용할 수 있어야 되는거 아닌가? 라고 생각할 수 있다. 논리적 상수성은 이런 부분을 해결해주며, 비트수준 상수성의 검열을 회피 위해 새로운 키워드인 mutable
을 사용한다.
mutable
은 말그대로 “돌연변이”이다. 비정적 멤버를 비트수준 상수성의 족쇄를 풀어주는 아름다운 키워드이다.
아래와 같이 바꿔서 테스트를 해보자.
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
// mutable 에 주목하라!
mutable std::size_t textLength; // 바로 직전에 계산한 텍스트 길이
mutable bool lengthIsValid; // 현재 길이가 유효한가?
};
std::size_t CTextBlock::length() const {
if (!lengthIsValid) {
textLength = std::strlen(pText); // 문제없이 동작함.
lengthIsValid = true; // 당연히 문제없다 ^_^
}
return textLength;
}
😴 포스팅 읽는 시간이 너무 길어지면 집중력이 떨어진다. 잠시 숨 좀 고르고🥺 다음 포스팅 을 보도록 하자. (다음 포스팅은 굉장히 짧다)