이부분은 정말 헷갈리는 부분이다. 우선 이것 부터 보자.
char greeting[] = "Hello"
char *p = greeting;
const char *p = greeting;
char * const p = greeting;
const char * const p = greeting;
자 각각의 p는 무슨 의미 일까? 우선 첫번째 p는 비상수 포인터, 비상수 데이터이다. const가 전혀 없는 걸로 봐서 한 눈에 알 수 있다. 두번째는 const가 char 앞에 붙어있다. 그래서 이것은 비상수 포인터, 상수 데이터이다. 세번째는 살짝 이해가 안갈 수 도 있는데 사실 char const* p와 동일한 표현이다. 그래서 상수 포인터, 비상수 데이터이다. 네번째는 딱보면 알겠지만 상수 포인터, 상수 데이터이다. 상수 포인터는 가리키는 주소가 상수화 되므로 가리키는 주소를 변경할 수 없다. 상수 데이터는 가리키는 데이터가 상수화 되므로 포인터에 접근하여 원본 데이터를 수정하는 일이 거부된다.
자 여기서 STL의 반복자 iterator는 T* 로 동작을 한다. 그런데 반복자를 const로 선언하면 반복자는 T* const와 같이 동작하게된다.
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin();
*iter = 10; // T* const이므로 값 수정 가능
++iter; // T* const이므로 가리키는 대상 변경 불가. 오류!
그래서 위와 같이 동작을 하게 된다. 그렇다면 만약 값 수정을 막고 싶을때는 어떻게 해야 할까? 바로 const_iterator를 쓰면 된다.
std::vector<int> vec;
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; // const T* 이므로 값 수정 불가능 오류!
++cIter; // const T* 이므로 가리키는 대상 변경 가능
그래서 만약 const std::vector<int>::const_iterator 로 선언을 하면 값도 변경 불가하고 가리키는 대상도 변경 불가능한 반복자가 된다.
다음으로는 가장 강력한 const의 용도인 함수 선언에 쓸 경우를 알아보자.
우선 const를 함수의 반환값으로 썼을 때의 경우이다.
class Number { ... };
const Number operator*(const Number& lhs, const Number& rhs);
예를 들어 위와 같이 Number라는 클래스를 만들고 각 클래스가 서로 곱해질 수 있도록 *연산자를 오버로딩 했다고 치자. 그런데 만약 위와 같이 반환 값을 const Number가 아니라 Number로 선언한다면 아래와 같은 불상사가 생길 수 도 있다.
Number a, b, c;
...
if (a * b = c) { ... }
여기서 나는 a * b의 값이 c와 같은지 비교하고 싶었을 뿐이다. (실수로 =을 하나 더 입력하지 않는 경우는 자주 있는 일이잖아요?) 그런데 여기서는 operator *의 반환값이 비상수 Number이기 때문에 아무런 오류가 발생되지 않고 a * b의 임시적인 반환 값 Number의 대입연산자가 발동하여 c의 값이 대입된후 수식이 잘 이루어 졌으므로 true가 넘겨져 항상 if문이 실행되는 기상천외한 일이 발생한다. 또한 오류도 나지 않고 잘 컴파일 되기 때문에 원인이 무엇인지 알아차리기 매우 힘들다. 그래서 만약 여기서 const Number를 반환하도록 operator*를 수정한다면 상수 값에 대입 연산자가 실행되어 컴파일이 되지 않고 오류가 발생해 오류를 바로 고칠 수 있을 것이다.
자 다음으로는 조금 어려운 내용에 들어가기 앞서 상수 멤버 함수에 대해 알아보자.
상수 멤버함수는 함수의 선언 뒤에 const를 붙여서 선언한다. 이 안에서는 멤버 값의 변경이 불가능 하다.
class TextBlock
{
public:
...
const char& operator[](std::size_t position) const
{
return text[position];
}
char* operator[](std::size_t position)
{
return text[position];
}
private:
std::string text;
};
상수 멤버 함수는 상수 객체에서 호출이 된다. 상수 객체에서는 일반적인 멤버 함수는 호출되지 않고 오로지 위와 같이 const를 붙인 상수 함수만 호출이 된다는점을 알아야 한다. 그래서 같은 기능을 상수와 비상수 객체 모두 지원하려면 위와같이 두 버전을 모두 구현 하면 된다.
자, 이것을 기억하고 이번에는 비트수준 상수성과 논리적 상수성에 대해 알아보자. 비트수준 상수성은 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 함수가 const임을 인정하는 개념이다. 한 마디로 함수 안에서 아무것도 변경하지 않은것을 의미 한다. 그런데 여기에는 한 가지 취약점이 있다. 바로 다음 코드를 보자.
class TextBlock
{
public:
...
char& operator[](std::size_t position) const
{
return text[position];
}
private:
std::string text;
};
자 여기서 operator[]는 상수 함수 이지만 반환 값이 char&이다. 즉 상수가 아니라는 점이다. 이 함수는 안에서 아무런 값도 변경하지 않았기 때문에 비트수준의 상수성을 지킨다. 그래서 반환 값이 const가 아니어도 허용이 된다. 여기서는 문제가 뭔지 모를 수 있지만 이제 이 객체를 상수로 선언하면 이야기가 달라진다.
const TextBlock ctb("Hello");
ctb[0] = 'J';
자 원래라면 이 코드는 바로 오류가 발생해야 한다. 상수 객체의 내부 값을 변경했기 때문이다. 하지만 operator[]가 비상수 참조자를 반환하였기 때문에 내부값 변경이 가능해진다. 그래서 이러한 황당한 상황을 보완하기 위해 나온것이 바로 논리적 상수성인데 솔직히 나는 이것이 이 상황을 어떻게 보완한다는건지는 잘 모르겠다. 그냥 const char&로 변경하면 충분히 막을 수 있을텐데 말이다. 아무튼간에 논리적 상수성이란 내부에서 값을 변경은 하되 사용자가 눈치 채지 못하게만 하면 상수성이 인정된다는 개념이다. 아래 예제를 보자.
class TextBlock
{
public:
...
std::size_t length() const;
private:
std::string text;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t TextBlock::length() const
{
if (!lengthIsValid) {
textLength = text.length();
lengthIsValid = true;
}
return textLength;
}
텍스트 블록의 길이를 반환하는 함수인데 글자의 길이를 계산하는 횟수를 최적화 하기 위해 위와 같이 코드를 짰다. 그런데 const가 선언된 상수함수 안에서는 값 변경이 불가능하다. 그래서 비트수준 상수성을 벗어나므로 오류가 발생한다. 하지만 여기서 mutable이라는 키워드를 이용하여 멤버 변수를 선언하면 비트 수준 상수성의 족쇄에서 벗어날 수 있다.
class TextBlock
{
public:
...
std::size_t length() const;
private:
std::string text;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t TextBlock::length() const
{
if (!lengthIsValid) {
textLength = text.length();
lengthIsValid = true;
}
return textLength;
}
자 이렇게 논리적 상수성을 이용한 함수가 하나 완성되었다. 그런데 문제는 코드의 중복현상이다. 상수객체용으로 함수를 또 생성하다보면 함수 내부가 거의 비슷하기마련이다. 그렇다면 어떻게 이 중복현상을 피할 수 있을까? 바로 비상수 멤버 함수에서 상수 멤버 함수를 호출하는 것이다. 말로만 하면 이해가 안가니 우선 코드를 보자.
class TextBlock
{
public:
...
const char& operator[](std::size_t position) const
{
...
...
...
return text[position];
}
char& operator[](std::size_t position)
{
return
const_cast<char&>(
static_cast<const TextBlock&>(*this)[position]
);
}
};
자 우선 상수 객체 버전으로 코드를 구현한 후 비상수 객체 버전에서는 정적 캐스팅을 이용해 자기 자신을 상수 객체로 변환한 후 상수버전 함수를 호출한 후 반환 값을 const_cast를 이용하여 비상수화를 시켜 반환하는 것이다. 솔직히 나는 이 코드를 보자마자 정말 감동을 했다. 어떻게 이런생각을 해낼 수 있느냔 말이다. 그런데 여기서 한가지 의문점이 들 수도 있다. 비상수 객체 버전 함수를 구현한 후 상수 객체에서 비상수 함수를 호출 하는 것은 안되나? 일것이다. 자 만약 그렇게 한다면 이미 상수인 객체에서 비상수 객체용 함수를 호출한다는 말인데 혹여나 우리가 비상수용 객체의 함수 내부에서 내부 값을 변경하는 일이 있었다면 상수 객체에서도 내부 값이 변경되는 일이 발생해 버릴 수 있기 때문에 절대로 그래서는 안된다.
'C++ > Effective C++' 카테고리의 다른 글
항목 7 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2024.01.18 |
---|---|
항목 6 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (2) | 2024.01.18 |
항목 4 객체를 사용하기 전에 반드시 그 객체를 초기화 하자 (0) | 2024.01.18 |
항목 2 #define을 쓰려거든 const, enum, inline을 떠올리자. (0) | 2024.01.17 |
Effective C++ 스타트 (0) | 2024.01.17 |