로또
3. 낌새만 보이면 const를 들이대 보자! 본문
작성 이유
const는 함수의 return 값이나 변수에 덧붙여 한 번 값이 설정된 이후 더 이상 변경을 하지 못하게 보장해주는 용도로 알고 있었는데, 해당 글을 읽고 여러 활용성에 대해 배울 수 있었다.
정의
const 키워드가 붙은 객체는 외부 변경을 불가능하게 하며, 컴파일러가 해당 제약을 단단히 지켜준다.
변수에 대한 const
int main(){
char greeting[] = "Hello";
// char *p = greeting; // 비상수 포인터, 비상수 데이터
// const char *p = greeting; // 비상수 포인터, 상수 데이터
// char * const p = greeting; // 상수 포인터, 비상수 데이터
const char * const p = greeting; // 상수 포인터, 상수 데이터
p[0] = 'X'; // p가 가리키는 값의 변경
p = nullptr; // p 자체의 값 변경 가능
return 0;
}
변수 선언 시 *을 기준으로 const가 좌측에 위치하면 포인터가 가리키는 대상을 상수 취급하고, 우측에 위치하면 포인터 자체를 상수 취급한다.
const가 좌측에 위치할 경우 포인터가 가리키는 greeting을 상수 취급하므로 p[0] = 'X'와 같이 사용할 경우 컴파일러에서 오류를 발생시킨다.
const가 우측에 위치할 경우, 포인터 p 자체를 상수 취급하므로 컴파일러에서 오류를 발생시킨다.

이와 비슷한 예시로, const iterator와 const_iterator가 존재한다.
#include <vector>
using namespace std;
int main(){
vector<int> v{1, 2, 3};
const vector<int>::iterator iter = v.begin(); // const * 처럼 동작하여 iter 자체를 상수 취급
vector<int>::const_iterator cIter = v.begin(); // * const 처럼 동작하여 cIter가 가리키는 대상 자체를 상수 취급
*iter = 10;
iter++; // iter의 값을 변경하므로 컴파일 에러 발생
*cIter = 10; // cIter가 가리키는 대상을 변경하므로 컴파일 에러 발생
cIter++;
return 0;
}
함수에 대한 const
일반적으로 함수의 return-type에 const를 사용할 수 있다는 사실을 알고 있을 것이다. 그외에도 다양한 사용 방법이 존재한다.
1. 멤버 함수 자체에 붙어 상수 객체에 의해서만 호출 가능한 함수를 의미한다.
#include <iostream>
using namespace std;
class MyClass{
public:
void func(){ // 일반 멤버 함수
cout << "func call" << endl;
}
void cfunc() const { // 상수 객체에 대해서만 호출될 수 있는 상수 멤버 함수
cout << "cfunc call" << endl;
}
};
int main(){
MyClass mc; // 비상수 객체
mc.func(); // 비상수 멤버 함수 호출 가능
mc.cfunc(); // 상수 멤버 함수 호출 가능
const MyClass cmc; // 상수 객체
cmc.func(); // 비상수 멤버 함수 호출 불가
cmc.cfunc(); // 상수 멤버 함수 호출 가능
}
2. 멤버 함수 자체에 붙어 "해당 함수 안에서는 어떠한 변수도 변경할 수 없음" 이라는 제약을 걸 수도 있다.
class MyClass{
public:
MyClass(int _a){
a = _a;
}
void func() const{ // 상수 멤버 함수
a = 100; // 상수 멤버 함수에서는 값의 변경이 불가능하므로 컴파일 에러가 발생한다.
}
private:
int a;
};
int main(){
MyClass mc(10);
mc.func();
return 0;
}
단, mutable keyword를 이용하면 상수 멤버 함수안에서도 변수 변경이 가능하다.
class MyClass{
public:
MyClass(int _a){
a = _a;
}
void func() const{
a = 100; // a가 mutable 변수로 선언되었으므로 상수 멤버 함수에서도 값의 변경이 가능하다.
}
private:
mutable int a;
};
int main(){
MyClass mc(10);
mc.func();
return 0;
}
3. const가 함수 자체에 붙음으로 오버로딩이 가능하다.
#include <iostream>
using namespace std;
class MyClass{
public:
MyClass(){}
void func(){
cout << "func" << endl;
}
void func() const{
cout << "func const" << endl;
}
private:
mutable int a;
};
int main(){
MyClass mc;
mc.func(); // 비상수 멤버 함수 호출, func
const MyClass cmc;
cmc.func(); // 상수 멤버 함수 호출, func const
return 0;
}

비트 수준 상수성(물리적 상수성)
어떤 멤버 함수가 그 객체의 어떤 데이터 멤버(정적 변수 제외)도 바꾸지 않아야 그 멤버 함수가 const임을 인정하는 개념.
해당 객체를 구성하는 비트 중 그 어떤 비트를 바꾸어서는 안된다.
하지만 다음과 같은 문제점을 가진다.
#include <iostream>
#include <cstring>
using namespace std;
class CTextBlock{
public:
CTextBlock(const char* str){
text = new char[strlen(str)];
strcpy(text, str);
}
~CTextBlock(){
if(text != nullptr){
delete text;
}
}
char& operator[](unsigned int position) const {
return text[position];
}
private:
char* text;
};
int main(){
const CTextBlock cctb("Hello"); // 상수 객체 생성
char *pc = &cctb[0]; // 상수 함수 호출
*pc = 'X'; // 상수 객체에 접근하여 값의 변경이 가능하다.
return 0;
}
위 코드처럼 상수 객체에 대해 상수 함수를 호출한 후, 상수 객체에 접근하여 값 변경이 가능하다는 것이다.
이러한 경우를 방지하기 위해 논리적 상수성이라는 개념이 등장하였다.
논리적 상수성
상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없게 하는 것이 아니라, 일부 몇 비트 정도는 바꿀 수 있되 그것을 사용자 측에서 눈치채지 못하게만 하면 상수 멤버 함수의 자격이 있다.
class CTextBlock{
public:
// ...
size_t length() const{
if(!text_length_is_valid){
text_length = strlen(text);
text_length_is_valid = true;
}
return text_length;
}
// ...
private:
char* text;
mutable size_t text_length; // 상수 멤버 함수에서도 값이 수정 가능한 mutable keyword 적용
mutable bool text_length_is_valid;
};
위 코드는 상수 멤버 함수에서 값을 변경한다. 이는 비트수준 상수성을 완벽히 위배하고 있지만 논리적 상수성을 만족한다.
CTextBlock의 상수 객체에 대해서는 아무런 문제없이 사용할 수 있을 것이다.
상수 / 비상수 멤버 함수 간 코드 중복 피하기
앞서, const keyword를 통해 오버로딩이 가능하다고 하였다. 이 때, 다음과 같은 코드 중복이 발생할 수 있을 것이다.
class TextBlock{
public:
const char& operator[](size_t position) const{
// ... 경계 검사
// ... 접근 데이터 로깅
// ... 자료 무결성 검증
return text[position];
}
char& operator[](size_t position){
// ... 경계 검사
// ... 접근 데이터 로깅
// ... 자료 무결성 검증
return text[position];
}
private:
string text;
};
상수 멤버 함수 operator[]는 const keyword를 제외하면 비상수 멤버 함수 operator[]와 정확히 하는 일이 동일하다.
비상수 멤버 함수 operator[]를 호출하는 쪽이라면 비상수 객체일 것이므로 상수 멤버 함수를 호출하여 casting을 통해 const를 제거하여도 안전하다.
즉, 다음과 같이 비상수 멤버 함수에서 상수 멤버 함수를 호출, casting을 통해 const 를 제거해줌으로 코드 중복을 피할 수 있다.
class TextBlock{
public:
const char& operator[](size_t position) const{
// ... 경계 검사
// ... 접근 데이터 로깅
// ... 자료 무결성 검증
return text[position];
}
char& operator[](size_t position){
return const_cast<char&>( // 2. return-type에서 const를 제거한다.
static_cast<const TextBlock&>(*this)[position] // 1. this에 const를 붙여 상수 멤버 함수인 operator[] 호출
);
}
private:
string text;
};
const_cast<new type>(expression) : 포인터 또는 참조형의 상수성을 제거한다.
static_cast<new type>(expression) : 논리적으로 변환 가능한 타입을 변경한다.
이것만은 잊지 말자!
- const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 준다. const는 어떤 유효 범위에 있는 객체에도 붙을 수 있으며 함수 매개변수 및 return type에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있다.
- 컴파일러 입장에서 비트수준 상수성을 지켜야 하지만, 프로그래머는 논리적 상수성을 이용하여 프로그래밍해야 한다.
- 상수 멤버 및 비상수 멤버 함수가 기능적으로 동일하게 구현되어 있을 경우에는 코드 중복을 피하기 위해 비상수 멤버 함수가 상수 멤버 함수를 호출하도록 만들자.
참고 자료
Effective C++ 제3판 - 항목 3: 낌새만 보이면 const를 들이대 보자!
https://blockdmask.tistory.com/240
[C++] const_cast (타입 캐스트 연산자)
안녕하세요. BlockDMask 입니다.오늘은 C++ 의 네가지 타입 캐스트 연산자 중에 (static_cast, const_cast, reinterpret_cast, dynamic_cast) const_cast 에 대해 알아보겠습니다.> const_cast 에 관한 기본 특성const_cast(expres
blockdmask.tistory.com
https://blockdmask.tistory.com/236
[C++] static_cast (타입캐스트 연산자)
안녕하세요 BlockDMask 입니다.오늘은 C++의 네가지 타입변환 연산자 static_cast, dynamic_cast, reinterpret_cast, const_cast 중 static_cast에 대해 알아보겠습니다. > static_cast 기본 형태 static_cast(대상); static_cast(exp
blockdmask.tistory.com
'책 > Effective C++' 카테고리의 다른 글
| 8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자. (0) | 2023.11.06 |
|---|---|
| 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자. (0) | 2023.11.04 |
| 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자. (0) | 2023.11.03 |
| 5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자. (1) | 2023.11.02 |
| 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자. (1) | 2023.11.01 |