- const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 준다. const는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수, 반환 타입에도 붙을 수 있고 멤버 함수에도 붙을 수 있다.
- 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 프로그래머는 논리적인 상수성을 사용하여 프로그래밍해야 한다.
- 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우 코드 중복을 피해야 한다.
이때 비상수 버전이 상수 버전을 호출하도록 만들어라
const
const는 '의미적인 제약'을 소스 코드 수준에서 붙인다. 또한, 컴파일러가 이 제약을 단단히 지켜준다.
const는 다양하게 활용할 수 있다. 클래스 바깥에서는 전역 혹은 네임스페이스 유효범위의 상수를 선언하는데 쓸 수 있다.
그뿐 아니라 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있다.
클래스 내부에서는 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있다.
포인터의 경우에도 포인터 자체를 상수로, 포인터가 가리키는 데이터도 상수로 지정할 수 있다.
포인터에서 const
char greeting[] = "Hello";
char *p = greeting; //비상수 포인터,
// 비상수 데이터
const char *p = greeting; // 비상수 포인터,
// 상수 데이터
char * const p = greeting // 상수 포인터,
// 비상수 데이터
const char * const p = greeting; // 상수 포인터,
// 상수 데이터
const가 *의 왼쪽에 있으면 포인터가 가리키는 대상이 상수이고 const가 *의 오른쪽에 있다면 포인터 자체가 상수이다.
STL 반복자(iterator)는 포인터를 본뜬 것이기 때문에, 기본적인 동작 원리가 T* 포인터와 흡사하다.
어떤 반복자를 const 선언하는 일은 포인터를 상수(T * const)로 선언하는 것과 같다.
반복자는 자신이 가리키는 대상이 아닌 것을 가리키는 경우가 허용되지 않지만, 반복자가 가리키는 대상 자체는 변경이 가능하다. (만약 변경이 불가능하게 하려면 const_iterator를 사용)
std::vector<int> vec;
...
const std::vector<int>::iterator itr = vec.begin(); // itr은 T* const처럼 동작
*itr = 10; // OK, itr이 가리키는 대상 변경
++itr; // Error, itr은 상수
std::vector<int>::const_iterator cItr = vec.begin(); // cItr은 const T*처럼 동작
*cItr = 10; // Error, *cItr은 상수
++cItr; // OK, cItr 변경
함수에서 const
const를 함수 선언시에 사용하면 효과적으로 사용할 수 있다.
const는 함수 반환 값, 매개변수, 멤버 함수, 함수 전체 등에 붙일 수 있다.
함수 반환 값을 상수로 정해 주면, 안전성이나 효율을 포기하지 않고도 에러를 줄일 수 있다.
class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);
operator*의 반환 값을 상수 객체로 넘겨주게 되면 다음과 같은 실수를 방지할 수 있다.
Rational a, b, c;
(a * b) = c; // a*b의 결과에 operator=를 호출
if( a * b = c) ... // 비교하려는 의도였지만 =를 하나만 작성
const 매개변수는 매개변수로 넘어온 변수를 변경하지 않는 상황에서 read만 하기에 적합하다.
멤버 함수에 붙는 const의 역할은 "해당 멤버 함수가 상수 객체에 대해 호출될 함수이다"라는 사실을 알려 주는 것이다.
이는 클래스의 인터페이스를 이해하기 좋게 하고 상수 객체를 사용할 수 있게 한다. 이는 코드의 효율에 아주 중요한 부분이다. c++ 프로그램의 실행 성능을 높이는 기법 중 하나인 "객체 전달을 상수 객체에 대한 참조자로 진행한다" 는 개념을 사용하려면 상수 상태로 전달된 객체를 조작할 수 있는 const 멤버 함수, 즉 상수 멤버 함수가 준비되어 있어야 한다.
const가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능하다.
class TextBlock {
public:
...
const char& operator[] (std::size_t position) const // 상수 객체에 대한 operator[]
{ return text[position]; }
char& operator[] (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[]의 상수 버전 호출
std::cout << tb[0]; // OK
tb[0] = 'x'; // OK
std::cout << ctb[0]; // OK
ctb[0] = 'x'; // Error, 상수는 변경 불가
주의해야 할 점은 operator[]의 반환 타입이 char의 참조자이다. 만약 char만 반환한다면 대입연산으로 값을 변경하는 코드는 컴파일되지 않는다. 기본제공 타입을 반환하는 함수의 반환 값을 수정하는 일은 절대 불가능하다.
만약 가능하더라도 C++은 '값에 의한 반환'을 수행하기 때문에 변경하는 값은 해당 객체의 복사본이다.
비트수준 상수성 & 논리적 상수성
비트수준 상수성은 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 된다는 개념이다.
즉, 그 객체를 구성하는 비트들 중 어떤 것도 바꾸면 안되는 것이다.
C++에서 정의하고 있는 상수성은 비트수준 상수성이다.
하지만, 제대로 const를 동작하는데는 비트수준 상수성 검사를 통과해도 아직 부족하다.
어떤 포인터가 가리키는 대상을 수정하는멤버 함수들은 비트수준 상수성을 통과한다. 하지만 논리적으로는 객체의 데이터가 변경된 것이다.
const CTextBlock cctb("Hello") // 상수 객체
char *pc = &cctb[0]; // 상수 버전 operator[]를 호출하여 내부 데이터에 대한 포인터 획득
*pc = 'J'; // 의도하지 않은 동작
이러한 모순을 보완하기 위해 논리적 상수성이 등장했다.
상수 멤버 함수라고 해서 객체의 한 비트도 수정하지 못하게 하는 게 아니라 일부 비트를 수정하더라도 사용자 측에서 알아채지 못하게 하면 상수 멤버로 인정하는 것이다.
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;
}
비트수준 상수성은 만족하지 못한다. 그렇지만 CTextBlock의 상수 객체에 대해서는 문제가 없이 동작한다.
하지만 컴파일이 불가능하다. 이를 해결하기 위해서는 mutable을 사용하면 된다.
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
mutable std::size_t textlength; // mutable을 붙여
mutable bool lengthIsValid; // 수정이 가능
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid) {
textLength = std::strlen(pText); // 상수 멤버 함수안에서 변수 변경 가능
lengthIsValid = true;
}
return textLength;
}
상수 멤버 함수 & 비상수 멤버 함수 코드 중복 피하기
class TextBlock {
public:
...
const char& operator[] (std::size_t position) const
{
...
...
...
return text[position];
}
const char& operator[] (std::size_t position)
{
...
...
...
return text[position];
}
private:
std::string text;
};
상수 멤버 함수와 비상수 멤버 함수가 하는 일은 똑같기 때문에 코드 중복이 발생했다.
이런 함수에서 하는 작업이 많다면 더욱 문제가 된다.
이러한 코드 중복은 캐스팅을 이용하여 해결할 수 있다.
하지만, 캐스팅은 안전하지 않지만 이러한 경우에서는 확신을 갖고 사용할 수 있다.
class TextBlock {
public:
...
const char& operator[] (std::size_t position) const
{
...
...
...
return text[position];
}
const char& operator[] (std::size_t position) // 상수 버전 op[] 호출만
{
return
const_cast<char&>( // const 제거
static_cast<const TextBlock&>(*this)[position] // *this의 타입에 const 붙이기
);
}
private:
std::string text;
};
*this의 타입을 캐스팅하여 TextBlock& 에서 const TextBlock&으로 바꾼다.(static_cast)
그렇게 만들어진 상수 객체로 상수 멤버 함수를 수행한 뒤 다시 const를 제거한다. (const_cast)
(비상수 객체를 상수 객체로 변환하는 것은 안전한 타입 변환이기 때문에 static_cast를 사용해도 무방하다)
하지만, 상수 객체를 비상수 객체로 변환하여 적용하는 것은 상수 멤버 함수라는 성질을 위반하는 것이다.