03: 낌새만 보이면 const를 들이대 보자!
const는 의미적인 제약을 코드 수준에서 붙인다는 점과 컴파일러가 이 제약을 단단히 지켜준다는 점이 아주 좋은 부분이다. const는 항목 02에서 처럼 전역 혹은 네임스페이스, 유효범위의 상수를 선언하는데도 쓸 수 있다.
const의 가장 강력한 용도는 함수 선언에 쓸 경우이다. 함수 반환값을 상수로 정해주면, 안정성이나 효율을 포기하지 않고도 사용자측의 에러 돌발 상황을 줄이는 효과를 꽤 자주 볼 수 있다.
class Rational {};
const Rational operator*(const Rational& lhs, const Rational& rhs);
Rational a, b, c;
if(a * b = c) {} // a * b == c의 오타
위처럼 const를 붙인 연산자 * 를 사용할 때, 오타가 발생하면 오류를 찾아낼 수 있다.
const 매개변수, const 지역 객체와 같은 경우에는 가능한 한 사용하도록 하자. 컴파일 에러를 줄일 수 있다.
상수 멤버 함수
멤버 함수에 붙는 const 키워드의 역할은 "해당 멤버 함수가 상수 객체에 대해 호출될 함수이다"라는 사실을 알려주는 것이다. 이런 함수가 중요한 까닭은 2가지이다.
첫째는 클래스의 인터페이스를 이해하기 좋게 하기 위해서인데, 그 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고, 또 변경할 수 없는 함수는 무엇인가를 사용자 쪽에서 알고 있어야 하는 것이다. (const를 붙임으로써 알 수 있도록)
둘째는, 이 키워드를 통해 상수 객체를 사용할 수 있게 하자는 것인데 코드의 효율을 위해 아주 중요한 부분이다. C++ 프로그램의 실행 성능을 높이는 핵심 기법 중 하나가 객체 전달을 '상수 객체에 대한 참조자(reference-to-const)'로 진행하는 것이기 때문이다. 그런데 이 기법을 제대로 사용하려면 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;
}
TextBlock tb("Hello");
const TextBlock ctb("Hello");
std::cout << tb[0];
tb[0] = 'x';
std::cout << ctb[0];
ctb[0] = 'x'; // 컴파일 에러! 상수 버전 쓰기
tb[0]에 대한 쓰기는 가능하지만 ctb[0]에 대한 쓰기는 불가능하다.
비트수준 상수성(bitwise constness), 논리적 상수성(logical constness)
비트수준 상수성은 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야(정적 멤버 제외) 그 멤버 함수가 const임을 인정하는 개념이다. 하지만 문제가 있다.
class CTextBlock
{
public:
char& operator[] (std::size_t position) const
{ return pText[position]; }
private:
char* pText;
};
const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J'; // cctb는 "Jello"가 된다
컴파일러는 operator[]에 대해 불평할 이유가 없다. 하지만 결과적으로 cctb는 "Jello"라는 값을 가지게 되었다.
이런 황당한 상황을 보완하는 대체 개념이 논리적 상수성이다. 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다는 것이다.
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;
}
mutable은 비정적 데이터 멤버를 비트수준 상수성의 족쇄에서 풀어주는 아름다운 오색약수 같은 키워드이다. 위의 textLength와 lengthIsValid에 mutable이 없다면 컴파일 에러가 발생한다.
상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법
상수/비상수 함수에 이런저런 코드를 넣어버리면 코드의 내용이 아주 커져버린다. 커진 코드를 따로 함수로 설정해도 같은 함수가 두번이 호출된다. 함수의 기능을 한번만 구현해 두고 이것을 두 번 사용하고 싶다면 캐스팅을 이용해보자.
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]
);
}
};
이렇게 *this에 const를 붙인다음 operator[]를 호출하고 const_cast를 통해 const를 제거하면 operator[]를 상수/비상수 함수 두 가지로 만들지 않아도 구현할 수 있다.
하지만 위 순서의 역순은 절대로 하지말자. *this의 const를 떼어내는 짓은 재앙의 씨앗이다.
이것만은 잊지 말자!
- const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 준다. cosnt는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있다.
- 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 우리는 논리적 상수성을 사용해서 프로그래밍해야 한다.
- 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만들자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 06: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2023.10.02 |
---|---|
[Effective C++] 05: C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (1) | 2023.10.02 |
[Effective C++] 04: 객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2023.09.30 |
[Effective C++] 02: #define을 쓰려거든 const, enum, inline을 떠올리자 (0) | 2023.09.28 |
[Effective C++] 01: C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2023.09.28 |