항목 3. 낌새만 보이면 const를 들이대 보자!
const - const 키워드가 붙은 객체는 외부 변경을 불가능하게 한다.
const 는 사용범위가 넓다.
char greeting[] = "Hello";
char *p = greeting; // 비상수 포인터, 비상수 데이터
const char *p = greeting; // 비상수 포인터, 상수 데이터
char * const p = greeting; // 상수 포인터, 비상수 데이터
const char * const p = greeting; // 상수 포인터, 상수 데이터
const 를 타입 왼쪽에 붙이는 사람들도 있고, * 앞에 붙이는 사람들도 있다. 의미차이는 없습니다. 따라서 아래 함수들이 받아들이는 타입은 같습니다.
void f1(const widget *pw); // f1은 상수 widget 객체에 대한 포인터를 매개변수로 취합니다.
void f2(widget const *pw); // f2도 마찬가지
두가지 형태 모두 현업에서 잘 사용된다. 눈에 익혀두자!
STL iterator 에서 const 활용
std::vector<int> vec;
...
// iterator 를 const 로 선언, 포인터를 상수로 선언하는 것 (T * const 포인터)
const std::vector<int>::iterator iter = vec.begin();
*iter = 10; // OK, iter 가 가리키는 대상을 변경
++iter; // 에러! iter는 상수
// 변경이 불가능한 객체를 가리키는 Iterator(const T * 포인터의 STL 대응물)
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; // 에러! *cIter가 상수이기 때문에
++cIter; // 이건 문제 없습니다. cIter 를 변경하니까
함수 선언에서 const 활용
class Rational { ... };
const Rational operator*(const Rational &lhs, const Rational &rhs);
Rational a, b, c;
...
if (a * b = c) ... // 이런 문제를 방지할 수 있다.... 누가 이렇게 하냐... 당연히 실수겠지!?
위 코드 처럼 무의식으로(?! 무지성...) 하는 실수를 컴파일 과정에서 막을 수 있습니다.
상수 멤버 함수
const 가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능합니다.(C++ 의 중요한 성질입니다.)
TextBlock 의 operator[]를 오버로드(overload)한 예제
class TextBloock {
public:
...
const char &operator[](std::size_t position) const // 상수객체에 대한 operator[]
{ return text[position]; }
char &oeprator[](std::size_t position) // 비상수 객체에 대한 operator[]
{ return text[position]; }
private:
std::string text;
};
TextBlock tb("Hello");
std::cout << tb[0]; // TextBlock::operator[] 의 비상수 멤버를 호출합니다.
const TextBlock ctb["World");
std::cout << ctb[0]; // TextBlock::operator[] 의 상수 멤버를 호출합니다.
void print(const TextBlock &ctb) {
std::cout << ctb[0]; // TextBlock:operator[] 의 상수 멤버 호출
}
TextBlock 의 상수 객체와 비상수 객체의 쓰임새
std::cout << tb[0]; // 비상수 버전의 객체를 읽습니다.
tb[0] = 'x'; // 비상수 버전의 객체를 씁니다.
std::cout << ctb[0]; // 상수버전의 객체를 읽습니다.
ctb[0] = 'x'; // 컴파일 에러! 상수버전의 객체에 대해 쓰기는 안됩니다.
4 번째 줄에서 발생한 에러는 oeprator[] 의 반환타입(return type) 때문에 생겼습니다. oeprator[] 호출이 잘못된 것은 없습니다.
const char & 타입에 대입 연산을 시도했기 때문에 에러가 발생했습니다.
위 코드에서 눈여겨 볼점, oeperator[] 의 비상수 멤버는 char 의 참조자(Reference) 를 반환합니다. 만약, char 만 사용했다면,
tb[0] = 'x';
위 코드가 컴파일 되지 않습니다. 기본제공 타입을 반환하는 함수의 반환 값을 수정하는 일은 절대로 있을 수 없기 때문입니다.
설령 이것이 합법적으로 통한다 해도, 반환 시 '값에 의한 반환'을 수행하는 C++의 성질이 뒤에 떡 버티고 있습니다.
tb.text[0] 의 사본이지, tb.text[0] 자체가 아닙니다.
비트수준 상수성[bitwise constness, 물리적 상수성(physical constness)] 과 논리적 상수성(logical constness)
비트수준 상수성 : 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야(정적 멤버는 제외합니다) 그 멤버 함수가 'const'임을 인정하는 개념
C++에선 비트수준 상수성으로 정의되어 있습니다. 그러나 포인터가 가리키는 대상을 수정하는 멤버 함수들은 이를 피할 수 있습니다.....
class CTextBlock {
public:
...
char &operator[](std::size_t position) const // 부적절한 operator[] 의 선언
{ return pText[position]; } // (비트수준 상수성이 있어서 허용)
private:
char *pText;
};
operator[] 함수가 상수 멤버 함수로 선언되어 있습니다.
그럼에도 불구하고 해당 객체의 내부 데이터에 대한 참조자를 버젓이 반환합니다.
const CTextBlock cctb("Hello"); // 상수 객체를 선언
char *pc = &cctb[0]; // 상수 버전의 operator[]를 호출하여 cctb의 내부 데이터에 대한 포인터를 얻습니다.
*pc = 'J'; // cctb는 이제 "Jello" 라는 값을 갖습니다.
확실히 잘못된 동작입니다... 상수 멤버 함수를 호출했더니 값이 변해버렸습니다.
논리적 상수성 : 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다는 것입니다.
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
std::size_t textlength;
bool lengthIsValid;
};
std::size_t CTextBlock::length() const {
if (!legnthIsValid) {
textLength = std::strlen(pText); // Error! 상수 멤버 함수 안에서는 대입 불가능!
lengthIsValid = true;
}
}
length 구현은 textLength 및 lengthIsValid 가 바뀔 수 있어서 '비트수준 상수성'과는 거리가 멉니다.
컴파일러를 통과하려면 비트 수준의 상수성을 지켜야합니다.
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
mutable std::size_t textlength; // 어떤 순간에도 수정 가능, 상수 멤버 함수 안에서도 쌉가능
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const {
if (!legnthIsValid) {
textLength = std::strlen(pText); // mutable 키워드로 인해서 에러 없음
lengthIsValid = true;
}
}
mutable 은 비정적 데이터 멤버를 비트수준 상수성의 족쇄에서 풀어줍니다.
상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법
위 코드를 수정하여 operator[] 의 상수/비상수 버전을 구현하면 코드 중복이 발생합니다.
class TextBloock {
public:
...
const char &operator[](std::size_t position) const // 상수객체에 대한 operator[]
{
... // 경계검사
... // 접근 데이터 로깅
... // 자료 무결성 검증
return text[position];
}
char &oeprator[](std::size_t position) // 비상수 객체에 대한 operator[]
{
... // 경계검사
... // 접근 데이터 로깅
... // 자료 무결성 검증
return text[position];
}
private:
std::string text;
};
캐스팅이 필요하긴 하지만, 안전성도 유지하면서 코드 중복을 피하는 방법은 비상수 operator[] 가 상수 버전 operator[] 를 호출하도록 구현하는 것 입니다.
class TextBloock {
public:
...
const char &operator[](std::size_t position) const // 이전과 동일
{
... // 경계검사
... // 접근 데이터 로깅
... // 자료 무결성 검증
return text[position];
}
char &oeprator[](std::size_t position) // 상수 버전 operator[] 를 호출
{
// op[] 의 반환 타입에 캐스팅을 적용, const 를 때어냅니다.
// *this의 타입에 const 를 붙입니다. op[]의 상수 버전을 호출합니다.
return const_cast<check &>(static_cast<const TextBlock &>(*this)[position]);
}
private:
std::string text;
};
*this 의 타입을 TextBlock & 에서 const TextBlock & 으로 바꿉니다. 그리고 operator[] 의 const 를 떼어버립니다.
위 방식을 뒤집어서 하면 안됩니다. 상수 멤버에서 비상수 멤버를 호출하는 것은 컴파일러가 에러를 반환합니다.
어떻게든 상수 멤버에서 비상수 멤버를 호출하려고 *this 의 const 를 떼어내는 것은 온갖 재앙의 씨앗입니다....
이것만은 잊지 말자!
- const 를 붙여 선언하면 컴파일러가 사용사의 에러를 잡아내는 데 도움을 줍니다. const 는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있습니다.
- 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 여러분은 개념적인(논리적인) 상수성을 사용해서 프로그래밍해야 합니다.
- 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은, 이때 비상수 버전이 상수 버전을 호출하도록 만드세요.
'Effective C++' 카테고리의 다른 글
[Effective C++] 2. #define을 쓰려거든 const, enum, inline 을 떠올리자 (0) | 2021.04.23 |
---|---|
[Effective C++] 1. C++ 를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2021.04.22 |