C++: const keyword는 어떻게 쓸까?

C나 C# 그리고 Java 같은 언어와 C++이 가장 많이 다른 점 중에 하나는 const 라는 키워드를 지원한다는 점이다. (Java의 final이 살짝 비슷하긴 하지만 목적과 기능이 확실히 다르다)

const 키워드가 지정할 수 있는 것은 2가지다.

  • 대상(변수)이 변하지 않는 무언가라고 지정한다.
  • 동작(함수, 연산자)이 동작의 주체를 변화시키지 않는다.

가장 간단한 예가 다음과 같은 거다.

void foo(const int bar);

이런 함수의 내부(body)에서는 bar의 값을 바꿀 수 없다.

직접적으로 변수가 아니라 포인터에 const가 붙게 되면 상수성을 부여할 수 있는 곳이 2가지가 된다. 우선 포인터가 가리키는(pointing하는) 주소를 바꿀 수 없게 할 수 있다. 그리고 나머지 한 가지로, 포인터가 가리키는 곳의 값이 바뀌지 않게할 수 있다.

흔히 사용하는 C-style 문자열은 const char *str = "C style string"; 이다. 가리키는 곳의 값이 바뀌지 않는 경우다. (str = another_c_style_string; 이 가능한 것. 저렇게 선언하면 해당 문자열 자체는 상수가 되니 char *str 처럼 사용하진 말기를)

반대로 int* const a = &some_int_type_variable; 처럼 선언해서 쓴 경우 가리키는 주소가 바뀌지 않는 경우다.(바로 이 경우가 C++의 참조자 – reference -와 같은 의미가 된다; 의미에 맞게 this 포인터는 언제나 T* const다)

그리고 const는 어떤 동작(함수, 연산자)에 붙어서, 해당 함수가 행해지는 객체(주어에 해당하는 객체)의 값이 바뀌지 않는 것을 보장하게 한다. 예를 들어 STL의 std::map의 원소 수가 0인지 확인하는 std::map::emtpy()의 함수의 경우, bool empty() const; 로 선언되어 있다. 즉 원소 수가 0개인지 확인하는 것은 map의 내용을 바꾸지 않는다는 것이다. (물론 mutable 변수는 바꿀 수 있지만 나중에 생각하자;) )

실제로 들어가면, return_type member_func() const; 의 경우 this 포인터가 T* const가 아니라 const T* const (가리키는 것도 못 바꾸고, 가리키는 주소자체도 변경할 수 없는 포인터)가 된다. 그래서 멤버 변수를 못 바꾸고, const가 안붙은 멤버 함수도 못 부른다.

그럼 이런 기능을 하는 const keyword를 어떤 목적으로 쓰는게 맞을까?

우선 함수의 인자를 “정제"하는 목적으로 사용할 수 있다. 지금 작성하는 코드를 나중에 다른 사람이 고칠 수도 있다고 했을 때, 함수의 인자를 원래 의미대로사용하게 강제할 수 있다 – 참조자로 넘어 온 것을 바꾸지 말아야하는데 바꾸거나 할 수 있으니.

그리고 이런 기능은 멤버 함수에 부여할 수 있기 때문에, 특정 타입의 객체의 특정 기능이 상수성을 보장하는지를 강제하는 강력한 도구가 된다. 멤버 함수가 const인 경우, 그 안에서는 (mutable이 아닌) 모든 멤버를 바꿀수 없고 -단 const가 아닌 인자는 예외 – , const 멤버 함수만 호출할 수 있게 된다.

실제 사용되는 예를 들어보면, A라는 타입이 B타입의 변수를 내부에 갖고 있는데, B타입이 struct 타입이라 수많은 변수를 가지고 있다고 치자. 그런데 이 B타입 변수엔 멤버 변수가 굉장히 많은데 대부분 read-only로 사용되고 일부만 사용된다 치자. C++은 C#이나 Python의 프로퍼티 기능을 지원하지 않기 때문에, 이에 대한 getter를 일일이 만들어야 할 수도 있다.

만약 B타입 변수를 통채로 값으로 복사해서 받는 경우는 어떨까? B타입의 크기가 크다면 non-sense가 된다. 복사 가능하다는 보장도 항상 있는게 아니고.

이때 사용되는 것이 const reference를 반환하는 const 멤버 함수다. 모든 getter들은 – 값 캐슁을 하지 않는 경우; 값 캐슁을 해도 mutable로 충분하면 포함하고 – const 멤버 함수여야 한다.(대부분의 경우 이게 맞다; 기억하자) 이 조건을 만족하면서도 복사하는 오버헤드가 없고, 번잡한 작업을 줄이는 것은 const reference 반환이 좋다 – 이 외에도 내부 변수를 외부에 보여줘야하는 경우엔 const 복사나 const refrence 반환이 적합하다.

pimpl idiom을 쓰는 경우에는 이런 경우에 해당할 수도 있고, 그렇지 않을 수도 있지만, 그렇지 않은 많은 경우 번잡한 getter들이나 내부 변수의 조심스러운 참조를 위해서는 const 키워드가 참 유용하다. (값 제한 면에서도 쓸모있긴 하지만)

ps. 이 글을 쓴 이유는 짐작이 가지 않는가? 어제 오후 내내 non-const 반환이 달린 코드 부분이 내 수많은 const 함수들에서 호출되야해서 코드를 싹 고치고 있어야 했다. 제발 C++이라면 const를 적절히 좀 써줘 Orz

ps2. ps에 대한 amihk군의 코멘트: 특히 const안붙은 코드를 자기가 고칠 권한이 없으면 더 (미치지)

Update: const 멤버 함수의 this 포인터 설명 추가 (thanks to lapiz), const 관련 사례(?)추가(thanks to amihk)