const 키워드는 생긴 것 답지 않게 그야말로 팔방미인입니다. 클래스 바깥에서는 전역 혹은 네임스페이스 유효범위의 상수를 선어(정의)하는 데 쓸 수 있습니다. 그뿐 아니라 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있습니다. 클래스 내부의 경우에는, 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있습니다. 


포인터는 어떨까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
char greeting[] = "Hello";
 
char *= greeting;                        // 비상수 포인터,
                                        // 비상수 데이터
 
const char *= greeting;                // 비상수 포인터,
                                        // 상수 데이터
 
char * const p = greeting;                // 상수 포인터,
                                        // 비상수 데이터
 
const char * const p = greeting            // 상수 포인터,
                                        // 
cs


변덕에 일평생을 바쳐온 사람이 만든 문법처럼 보이지만 그렇지 않습니다. 잘 보세요 const가 *의 왼쪽에 있으면 포인터가 가리키는 대상이 상수인 반면, const 가 *의 오른쪽에 있는 경우엔 포인터 자체가 상수입니다. const가 *의 양쪽에 다있으면 포인터가 가리키는 대상 및 포인터가 다 상수라는 뜻이죠


포인터가 가리키는 대상을 상수로 만들 때 const를 사용하는 스타일은 조금씩 다릅니다. 어떤 프로그래머는 타입 앞에 const를 붙이기도 합니다. 그 외에는 타입의 뒤쪽이자 *의 앞에 const를 붙이는 사람들이겠지요. 의미적인 차이는 전혀 없습니다.


1
2
3
4
void f1(const Widget *pw);            // f1은 상수 Widget 객체에 대한
                                    // 포인터를 매개변수로 취합니다.
 
void f2(Widget const *pw);            // f2도 그렇고요.
cs


두 가지 형태 모두가 현업 개발자들의 코드에 아주 잘 쓰이고 있으므로, 눈이 어색해 하지 않도록 잘 익혀 두도록 합시다.


STL 반복자(iterator)는 포인터를 본뜻것이기 때문에, 기본적인 동작 원리가 T* 포인터와 진짜 흡사합니다. 어떤 반복자를 const로 선언하는 일은 포인터를 상수로 선언하는 것(즉 T* const 포인터)과 같습니다. 반복자는 자신이 가리키는 대상이 아닌 것을 가리키는 경우가 허용되지 않지만, 반복자가 가리키는 대상 자체는 변경이 가능합니다. 만약 변경이 불가능한 객체를 가리키는 반복자(즉, const T* 포인터의 STL 대응물)가 필요하다면 const_itertor를 쓰면 됩니다.



1
2
3
4
5
6
7
8
9
10
11
12
std::vector<int> vec;
...
 
const std::vector<int>::iterator iter = vec.begin();
                                    // iter는 T* const처럼 동작을 합니다.
*iter = 10;                            // OK, iter가 가리키는 대상을 변경합니다.
++iter;                                // 에러! iter는 상수입니다.
 
std::vector<int>::const_iterator cIter = vec.begin();
                                    // cIter는 const T*처럼 동작합니다.
*cIter = 10;                        // 에러! *cIter가 상수이기 때문에 안 됩니다.
++cIter;                            // 이건 문제없습니다. cIter를 변경하니까요.
cs




뭐니 뭐니 해도 가장 강력한 const의 용도는 함수 선언에 쓸 경우 입니다. 함수 선언문에 있어서 const는 함수 반환 값, 각각의 매개변수, 멤버 함수 앞에 붙을 수 있고, 함수 전체에 대해 const의 성질을 붙일 수 있습니다.


함수 반환 값을 상수로 정해 주면, 안전성이나 효율을 포기하지 않고도 사용자측의 에러 돌발 상황을 줄이는 효과를 꽤 자주 볼 수 있게 됩니다. 한 예로, 항목 24에서 살펴볼 유리수 클래스에서 operator* 함수가 어떻게 선언되어 있는지를 보도록 하지요.


1
2
3
class Rational {...}
 
const Rational operator* (const Rational& lhs, const Rational& rhs);
cs


첫인상이 까칠하다고 느끼는 프로그래머들이 꽤 있을 것 같습니다. operator* 의 반환 값이 상수 객체일 이유를 어디서 찾겠느냐는 의견일 텐데요, 그런데 상수 객체로 되어 있지 않으면 사용자 쪽에서 저지르는 아래와 같은 어처구니없는 실수를 멍청히 지켜볼 수밖에 없게 됩니다.


1
2
3
4
5
6
Rational a, b, c;
 
...
 
(a * b) = c;            // a*b의 결과에 대고
                        // operator=를 호출하다니요!
cs


두 수의 곱에 대입 연산을 취하고 싶은 프로그래머가 있을지는 모르겠지만, 이런 상황을 원하지 않았는데도 무의식중에 경험해 본 프로그래머가 전자보다 훨씬 많다는 사실을 압니다. 어쩌다가 그냥 키보드를 잘못 누른 것이죠(그리고 bool로 암시적 변환이 가능한 타입도 포함됩니다).


1
if ( a* b = c) ...        // 어흑, 나는 원래 비교하려고 그랬던 건데!!
cs


위의 코드는 a 및 b의 타입이 기본 제공 타입이었다면 용서 없이 문법 위반에 걸리는 코드입니다. 훌륭한 사용자 정의 타입들의 특징 중 하나는 기본제공 타입과의 쓸데없는 비호환성을 피한다는 것인데(항목 18 참조), 위에서 본 바와 같이 두 수의 곱에 대해 대입 연산이 되도록 놓아두는 것이 바로 '쓸데없는' 경우가 되겠습니다. operator*의 반환 값을 const로 정해 놓으면 이런 경우를 미연에 막을 수 있지요 그렇기 때문에 상수 반환 값 지정이 정답이 되는 것이고요.


const 매개변수에 대해선 특별히 새로 할 이야기는 없습니다. 그냥 const 타입의 지역객체와 특성이 똑같으니까요 그리고 이것 역시 가능한 한 항상 사용하도록 하십시오. 매개변수 혹은 지역 객체를 수정할 수 없게 하는 것이 목적이라면 const로 선언하는 것을 잊지 말도록 합시다. 눈 딱 감고 여섯 글자만 더 눌러 보세요. 여러분의 참한 심성을 건드리는 "==로 생각하고 친 건데 실수로 =를 쳤네"와 비슷한 컴파일 에러를 보게 될 일이 없을 것 입니다.




상수 멤버 함수


멤버 함수에 붙는 const 키워드가 중요한 이유는 무려 두가지 입니다. 첫째는 클래스의 인터페이스를 이해하기 좋게 하기 위해서인데, 그 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고, 또 변경할 수 없는 함수는 무엇인가를 사용자 쪽에서 알고 있어야 하는 것입니다. 둘째는 이 키워드를 통해 상수 객체를 사용할 수 있게 하자는 것인데, 코드의 효율을 위해 아주 중요한 부분이기도 합니다.


C++ 프로그램의 실행 성능을 높이는 핵심 기법 중 하나가 객체 전달을 '상수 객체에 대한 참조자(reference-to-const)'로 진행하는 것이기 때문이죠. 그런데 이 기법이 제대로 살아 움직이려면 상수 상태로 전달된 객체를 조작할 수 있는 const 멤버 함수, 즉 상수 멤버 함수가 준비되어 있어야 한다는 것이 바로 포인트 입니다.


const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능합니다. 이 성질을 잘 모르고 지나치는 분들이 꽤 있는 것 같은데, C++의 아주 중요한 성질이니 꼭 외워두세요. 한 예로, 문서의 한 구역을 나타내는 데 쓰려고 만든 클래스를 한번 보시죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
class TextBlock{
public:
    ...
 
    const char& operator[](std::size_t position) const        // 상수 객체에 대한
    { return text[position]; }                                // operator[]
 
    char& operator[](std::size_t position)                    // 비상수 객체에 대한
    { return text[position]; }                                // operator[]
 
private:
 
};
cs


위처럼 선언된 TextBlock의 operator[]는 다음과 같이 쓸 수 있습니다.


1
2
3
4
5
6
7
8
9
TextBlock tb("Hello");
std::cout << tb[0];                // TextBlock::operator[]의
                                // 비상수 멤버를 호출합니다.
 
const TextBlock ctb("World");
std::cout << ctb[0]                // TextBlock::operator[]의
                                // 상수 멤버를 호출합니다.
 
 
cs


참, 실제 프로그램에서 상수 객체가 생기는 경우는 1. 상수 객체에 대한 포인터 혹은 2. 상수 객체에 대한 참조자로 객체가 전달될 때입니다. 위의 ctb 예제는 이해를 돕기 위한 용도의 성격이 짙고, 아래의 예제가 더 실제의 경우와 가깝습니다.


1
2
3
4
5
void print (const TextBlock& ctb)            // 이 함수에서 ctb는 상수 객체로 쓰입니다.
{
    std::cout << ctb[0];                    // TextBlock::operator[]의 상수
    ...
}
cs


operator[ ]를 '오버로드(overload)'해서 각 버전마다 반환 타입을 다르게 가져갔기 때문에, TextBlock의 상수 객체와 비상수 객체의 쓰임새가 달라집니다.


1
2
3
4
5
6
7
8
9
10
11
std::cout << tb[0]            // 좋습니다. 비상수 버전의
                            // TextBlock 객체를 읽습니다.
 
tb[0= 'x';                // 역시 문제없죠. 비상수 버전의
                            // TextBlock 객체를 씁니다.
 
std::cout << ctb[0]            // 이것도 됩니다. 상수 버전의
                            // TextBlock 객체를 읽습니다.
 
ctb[0= 'x';                // 컴파일 에러 발생! 상수 버전의
                            // TextBlock 객체에 대해 쓰기는 안됩니다.
cs


이 에러는 const char& 타입에 대입 연산을 시도했기 때문에 생긴 것입니다. 상수 멤버로 되어 있는 operator[]의 반환 타입이 const char&이니까요.




어떤 멤버 함수가 상수 멤버(const)라는 것이 대체 어떤 의미일까요?

여기에는 굵직한 양대 개념이 있습니다. 하나는 비트수준 상수성[bitwise constness, 다른말로 물리적 상수성(physical constness)이라고함]이고, 또하나는 논리적 상수성 입니다.


비트수준 상수성은 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야(정적 멤버는 제외합니다.) 그 멤버 함수가 'const'임을 인정하는 개념입니다. 즉, 그 객체를 구성하는 비트들 중 어떤 것도 바꾸면 안 된다는 것입니다.


그런데, 애석하게도 '제데로 const'로 동작하지 않는데도 이 비트수준 상수성 검사를 통과하는 멤버 함수들이 적지 않다는 점이 우리를 슬프게 합니다.


1
2
3
4
5
6
7
8
9
10
11
class CTextBlock {
public:
    ...
 
    char& operator[] (std::size_t position) const    // 부적절한(그러나 비트수준
    { return pText[position]; }                        // 상수성이 있어서 허용되는)
                                                    // operator[]의 선언
 
private:
    char *pText;
};
cs


operator[] 의 내부 코드만 보면 pText는 안 건드린다는 점은 확실합니다. 그러니 컴파일러가 이 operator[]에 대한 코드를 생성할 떄 불평할 이유가 없겠지요.


하지만 이로 인해 어떤 사태가 생길지는 아래에서 확인해 보도록 합시다.


1
2
3
4
5
6
const CTextBlock cctb("Hello");        // 상수 객체를 선언합니다.
 
char *pc = &cctb[0];                // 상수 버전의 operator[]를 호출하여 cctb
                                    // 내부 데이터에 대한 포인터를 얻습니다.
 
*pc = 'J';                            // cctb는 이제 "Jello"라는 값을 갖습니다.
cs


확실히 무엇인가가 잘못됐습니다. 어떤 값으로 초기화된 상수 객체를 하나 만들어 놓고 이것에다 상수 멤버 함수를 호출했더니 값이 변해버린 것입니다!




논리적 상수성이란 개념은 이런 황당한 상황을 보완하는 대체 개념으로 나오게 되었습니다. 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇비트 정도는 바꿀수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다는 것입니다.


예를 들어


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;                    // textLength 및 lengthIsValid에
    }                                            // 대입할 수 없습니다.
 
    return textLength;
}
cs


CTextBlock의 상수 객체에 대해서는 당연히 아무 문제가 없어야 할 것 같은 코드 입니다. 컴파일러는 에러를 쏟아낼 게 뻔합니다. 이런 상황에서는 어떻게 해야 할까요?


바로 mutable을 사용하는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 (!lengthIsValid) {
        textLength = std::strlen(pText);        // 이제 문제 없습니다.
        lengthIsValid = true;                    // 당연히 문제없죠.
    }
 
    return textLength;
}
cs






상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CTextBlock {
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;
};
cs


출석 부르기도 무서운 코드 중복입니다. 코드 중복과 함꼐 꼭 따라 나오는 말썽꾸러기 친구 녀석들이 더 큰 골치입니다. 컴파일 시간, 유지보수, 코드 크기 부풀림... 감당할 수 있겠어요?

경계 검사 등의 자질구레한 코드를 별도의 멤버 함수(물론 private멤버로)에 옮겨 두는 방법도 제법 괜찮을 것 같다고 생각 하겠지만 return문은 또 중복 코드 아닙니까? 자신을 속이지 마세요.



코드 중복을 피하는 방법은 비상수 operator[] 가 상수 버전을 호출하도록 구현하는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class TextBlock {
public:
    ...
 
    const char& operator[] (std::size_t position) const
    {
        ...                                    // 경계 검사
        ...                                    // 접근 데이터 로깅
        ...                                    // 자료 무결성 검증
        return text[position];
    }
 
 
    char& operator[] (std::size_t position)
    {
        return
            const_cast<char&>(                                        // op[]의 반환 타입에 캐스팅을 적용,
                                                                    // const를 떼어냅니다.
                static_cast<const TextBlock&>(*this)[position]        // *this의 타입에 const를 붙입니다.
            );                                                        // op[]의 상수 버전을 호출합니다.
    }
    
    ...
 
};
cs


이런저런 걸림돌을 넘고 넘어, 어쨌든 연산자 함수 하나를 호출하는 것이 전부입니다. 그래서인지 문법이 조금 이상해 보이기도 하죠. 예쁜이 코드 대횡에 나가더라도 똑 떨어질 게 분명하지만, 코드 중복을 피하자는 원하는 효과를 얻을 수 있었습니다.


또한 중요한것이 있는데 상수 멤버 함수에서 비상수 멤버 함수를 호출하면 안된다는 것 입니다. 이는 그 객체를 수정하지 않겠다고 약속한것을 배신하는 셈이 되고 그 객체는 변경될 위험에 빠질 수 있습니다.



이것만은 잊지 말자!!

*const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 줍니다. const는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있습니다.


*컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 여러분은 개념적인(논리적인)상수성을 사용해서 프로그래밍해야 합니다.


*상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만드세요.


*할 수 있으면 아끼지 말고 const 남발하세요.












+ Recent posts