KoreanFoodie's Study
Effective C++ | 항목 3 : 낌새만 보이면 const 를 들이대 보자! 본문
Effective C++ | 항목 3 : 낌새만 보이면 const 를 들이대 보자!
GoldGiver 2022. 10. 25. 16:02
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 3 : 낌새만 보이면 const 를 들이대 보자!
핵심 :
1. const 를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 준다. const 는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있다.
2. 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 우리는 개념적인(논리적인) 상수성을 사용해서 프로그래밍해야 한다.
3. 상수 멤버 빛 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만들자.
다음과 같은 상수 포인터 관련 코드를 보자.
char greeting[] = "Hello";
// 비상수 포인터, 비상수 데이터
char *p = greeting;
// 비상수 포인터, 상수 데이터
cosnt char *p = greeting;
// 상수 포인터, 비상수 데이터
char * const p = greeting;
// 상수 포인터, 상수 데이터
const char * const p = greeting;
const 키워드가 * 표의 왼쪽에 있으면 포인터가 가리키는 대상이 상수이고, const 가 * 표의 오른쪽에 있는 경우에는 포인터 자체가 상수이다.
STL 의 반복자(iterator) 는 포인터를 본뜬 것이라서, 기본적인 동작 원리가 T* 포인터와 흡사하다. 즉, iterator 를 const 로 선언한다는 것은 T* const 포인터와 같다. 반복자는 자신이 가리키는 대상이 아닌 것을 가리킬 수는 없지만, 가리키는 대상을 변경할 수는 있다. 만약 변경이 불가능한 객체를 가리키는 반복자를 정의하고 싶으면 const_iterator 를 쓰면 된다!
std::vector<int> vec;
// iter 는 T* const 처럼 동작한다
const std::vector<int>::iterator iter = vec.begin();
*iter = 10; // OK, iter 가 가리키는 대상의 값을 변경한다
++iter; // 에러! iter 포인터 자체는 상수이므로 다른 것을 가리킬 수 없다
// iter 는 T* const 처럼 동작한다
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; // 에러! *cIter 가 상수이기 때문에 안 된다.
++cIter; // 이건 문제 없다. cIter 를 변경하기 때문이다!
const 를 반환 값으로 적어주면, == 를 써야 하는 경우, = 를 써서 에러가 나는 어처구니 없는 실수를 방지할 수 있다.
상수 멤버 함수
멤버 함수에 붙는 const 키워드의 역할은 "해당 멤버 함수가 상수 객체에 대해 호출될 함수이다" 라는 사실을 알려주는 것이다. 이렇게 사용하면, 두 가지 장점이 있다.
- 클래스의 인터페이스를 이해하기 좋게 만들 수 있다.
- 이 키워드를 통해 상수 객체를 사용할 수 있게 만들 수 있다.
C++ 프로그램의 실행 성능을 높이는 핵심 기법 중 하나가 객체 전달을 '상수 객체에 대한 참조자(reference-to-const)' 로 진행하는 것인데, 이 기법을 제대로 쓰려면 상수 상태로 전달된 객체를 조작할 수 있는 const 멤버 함수, 즉 상수 멤버 함수가 준비되어 있어야 하기 때문이다.
const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능하다!
다음과 같은 예시를 보자.
class TexsBlock
{
public:
...
// 상수 객체에 대한 operator[]
const char& operator[](std::size_t position) const
{ return text[position]; }
// 비상수 객체에 대한 operator[]
char& operator[] (std::size_t position)
{ return text[position]; }
private:
std::string text;
};
...
// 객체 정의
TextBlock tb("Hello");
const TextBlock ctb("Hello");
// 비상수 버전의 TextBlock 객체를 읽는다
std::cout << tb[0]
// 비상수 버전의 TextBlock 객체를 쓴다
tb[0] = 'x';
// 상수 버전의 TextBlock 객체를 읽는다
std::cout << ctb[0];
// 컴파일 에러! : 상수 버전의 TextBlock 객체를 쓴다
ctb[0] = 'x';
어떤 멤버 함수가 상수(cosnt) 라는 것은, 두 가지 의미를 가진다.
- 비트수준 상수성(bitwise constness, 다른 말로 물리적 상수성(physical constness))
- 논리적 상수성 (logical constness)
비트 수준 상수성은 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 한드는 뜻으로, 컴파일러는 데이터 멤버에 대해 대입 연산이 수행되었는지만 보면 된다(사실 C++ 에서 정의하고 있는 상수성이 비트수준 상수성이다).
비트 수준 상수성만 가지고는, 다음과 같은 문제를 막을 수 없다.
class CTextBlock
{
public:
// 부적절한 (그러나 비트수준 상수성이 있어 허용되는) operator[] 의 선언
char& operator[](std::size_t position) const
{ return pText[position]; }
private:
char *pText;
}
...
// 상수 객체를 선언한다
const CTextBlock cctb("Hello");
// 상수 버전의 operator[] 를 호출하여
// cctb 의 내부 데이터에 대한 포인터를 얻는다
char *pc = &cctb[0];
// 이럴수가! cctb 는 이제 "Jello" 라는 값을 갖는다!
*pc = 'J';
논리적 상수성이라는 개념으로 이런 상황을 보완할 수 있다. 즉, 상수 멤버 함수는 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다는 것이다. 길이를 상수로 취급하는 다음 예시 코드를 보자.
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)
{
// mutable 로 선언했으므로 멤버 변수 수정이 가능하다!
textlength = std::strLen(pText);
lengthIsValid = true;
}
return textlength;
}
상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법
위의 operator[ ] 에서, 상수 버전 함수 호출을 위해 매번 비상수 버전의 코드를 복사 붙여넣기 해야 한다면 코드가 끔찍하게 증식할 것이다. 이 방법을 회피하는 방법은, 비상수 버전 함수가 상수 버전 함수를 호출하도록 구현하는 것이다.
class TextBlock
{
public:
// 이전과 동일
const char& operator[] (std::size_t position) const
{
...
return text[position];
}
// 상수 버전 op[] 를 호출하고 끝
char& operator[](std::size_t position)
{
return
// op[] 의 반환 타입에 캐스팅을 해서 const 를 떼어낸다
const_cast<char&>
(
// *this 타입에 const 를 붙인다
// (자기 자신이 아닌 const 버전 op[] 호출을 위해)
static_cast<const TextBLock&>
// op[] 의 상수 버전을 호출한다
(*this)[position]
);
}
}
비상수 멤버 함수에서 상수 멤버 함수를 호출하는 것은 논리적으로 문제가 없지만, 상수 멤버 함수에서 비상수 멤버 함수를 호출하는 것은 여러 문제를 야기할 수 있다!
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective C++ | 항목 6 : 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2022.10.25 |
---|---|
Effective C++ | 항목 5 : C++ 가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (0) | 2022.10.25 |
Effective C++ | 항목 4 : 객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2022.10.25 |
Effective C++ | 항목 2 : #define 을 쓰려거든 const, enum, inline 을 떠올리자 (0) | 2022.10.25 |
Effective C++ | 항목 1 : C++ 를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2022.10.25 |