#11. [레거시 C++ 가이드] 상수 한정자(const), 변경 가능 지정자(mutable), 최적화 제한 한정자(volatile)
모던 C++
- (C++11~) constexpr이 추가되어 컴파일 타임 프로그래밍이 강화됐습니다.
- (C++20~) volatile의 일부가 deprecate되었습니다.
개요
const인 개체나 함수를 사용하면, 메모리의 수정이 없으므로 예외가 발생하지 않습니다. 예외에 안전한 프로그램을 위해, 할 수 있는 한 최대한 많이 const로 작성하는 것이 좋습니다.
또한, 상수성 계약 을 준수하세요. 계약 위반시 대부분 컴파일 오류가 발생하니, 계약을 지키기 까다롭지도 않습니다.
항목 | 내용 |
---|---|
상수 개체 | 개체 정의시 const 한정자를 붙여 상수 개체를 만들 수 있습니다. 상수 개체는 값을 변경할 수 없습니다. 생성시 초기화 해야 합니다. |
상수 멤버 함수 | 멤버 함수의 뒤에 const 한정자를 붙여 상수 멤버 함수를 만들 수 있습니다. 상수 멤버 함수는 개체의 멤버 변수를 변경할 수 없습니다. 단, mutable로 정의된 개체는 수정 가능합니다. |
상수성 계약
상수성 계약은 값을 변경하지 않는다는 계약입니다. 메모리를 읽기만 하고 수정하지 않는다는 것이죠. 그러다 보니 예외를 발생시키지 않습니다. 논리적 상수성인 경우만 빼고요.
- 상수 개체는 생성과 함께 초기화 되어야 합니다.(컴파일 오류)
- 상수 개체는 변경할 수 없습니다.(컴파일 오류)
- 상수 멤버 함수는 멤버 변수를 수정하지 않습니다.(컴파일 오류, 단 mutable로 수정 가능합니다.)
- 상수 멤버 함수는 멤버 변수를 몰래 수정할 수 있는 포인터나 참조자를 리턴하지 않습니다.(컴파일 오류. 단, const_cast로 억지로 구현 가능하나 하지 마세요.)
- 상수 멤버 함수는 내부 구현에서 상수 멤버 함수만을 호출합니다.(컴파일 오류)
- 상수 멤버 함수는 예외가 발생하지 않습니다.(일반적으로 예외가 발생하지 않습니다만, 복잡한 연산들이 있다면, 노력해서 예외가 발생하지 않도록 구현해야 합니다.)
- mutable로 논리적 상수성을 구현한 경우 예외가 발생할 수 있습니다.(이때에는 예외 처리를 해야 합니다.)
한마디로 상수는 상수만 접근한다로 생각하면 됩니다. 한편, 비 상수는 바이러스처럼 전파됩니다. 비 상수 멤버 함수의 비 상수성 전파를 참고하세요.
상수 개체
상수 개체는 const를 붙여 정의합니다. 반드시 초기값을 설정해야 합니다.
1
2
const int x; // (X) 컴파일 오류. 초기값 없음
const int x = 10; // (O) x의 값은 이제 변경될 수 없음
포인터(혹은 참조자)형 변수의 경우 const*
와 같이 const의 오른쪽에 *
이 있으면 포인터가 참조하는 실제 데이터가 상수이고, 왼쪽에 *
이 있으면 포인터형 변수가 상수입니다.(포인터(Pointer)와 참조자(Reference) 언급)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int obj = 10;
int* p1 = &obj; // *p1 수정 가능. p1 수정 가능
*p1 = 20;
const int* p2 = &obj; // *p2 수정 불가. p2 수정 가능
*p2 = 20; // (X) 컴파일 오류
p2 = p1;
int* const p3 = &obj; // *p3 수정 가능. p3 수정 불가
*p3 = 20;
p3 = p1; // (X) 컴파일 오류
const int* const p4 = &obj; // *p4 수정 불가. p4 수정 불가
*p4 = 20; // (X) 컴파일 오류
p4 = p1; // (X) 컴파일 오류
복사 대입시 최상위 const 제거
최상위 const란 포인터나 참조자가 아닌 개체 타입의 const를 말합니다. const int*
나 const int&
말고 const int
처럼요.
최상위 const인 상수 개체는 그 값을 수정할 수 없지만, 복사 대입하여 복제본을 만들면 상수성을 제거할 수 있습니다.
1
2
3
4
5
6
7
8
9
const int constVal = 10;
constVal = 20; // (X) 컴파일 오류. 상수 개체는 수정할 수 없습니다.
const int a = constVal;
a = 20; // (X) 컴파일 오류. 상수 개체를 상수 개체로 복사 대입하면 수정할 수 없습니다.
int b = constVal;
b = 20; // (O) 상수 개체를 복사 대입하면 복제본은 수정할 수 있습니다.
EXPECT_TRUE(b == 20 && constVal == 10);
하지만, 포인터나 참조자로 대입받는 것은 복사 대입이 아니므로 상수성을 유지해야 합니다.(const_cast로 억지로 상수성을 땔 수는 있습니다만 하지 마세요.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int data = 10;
const int* constPtr = &data;
const int& constRef = data;
int* a = constPtr; // (X) 컴파일 오류. 상수 개체 포인터는 상수 개체 포인터로 받아야 합니다.
const int* b = constPtr; // (O)
int& c = *constPtr; // (X) 컴파일 오류. 상수 개체 포인터는 상수 개체 참조자로 받아야 합니다.
const int& d = *constPtr; // (O)
int& e = constRef; // (X) 컴파일 오류. 상수 개체 참조자는 상수 개체 참조자로 받아야 합니다.
const int& f = constRef; // (O)
int* g = &constRef; // (X) 컴파일 오류. 상수 개체 참조자는 상수 개체 포인터로 받아야 합니다.
const int* h = &constRef; // (O)
최상위 const인 상수 개체의 복사 대입시 복제본의 상수성이 제거될 수 있는 것은 참 당연한 것입니다. 복제본이니까요. 그런데, 이 당연한 규칙 때문에 복잡한 규칙들이 파생됩니다.
-
오버로딩된 함수 결정 규칙에서 최상위 const는 인자 타입에서 제거하고 취급합니다.
예를 들어 다음의 함수를 보면 인자가
int
와const int
로 동일하지만, 둘다int
타입의 인수나const int
타입의 인수를 대입받을 수 있습니다. 오버로딩된 함수 결정이 모호해지므로 아예 이둘을 동일 함수로 취급합니다.1 2
int f(int a); int f(const int a);
-
함수 템플릿 인수 추론에서 최상위 const는 무시됩니다.
템플릿 인스턴스화 할때
int
와const int
가 구분된다면, 상기 1번 문제가 똑같이 발생하게 됩니다. 따라서,const int
를 전달하더라도 최상위 const는 무시하고int
로 취급합니다.1 2 3 4 5
template<typename T> void f(T) {} const int a = 0; f(a); // f<int>(int)
리턴값의 상수성
리턴값에 무의미하게 const를 붙일 필요는 없습니다. 의미에 맞게 붙이거나 떼야 합니다.
리턴값이 기본 타입이라면 어짜피 리턴값이 복제되므로, const를 붙일 필요가 없거든요.
1
2
3
4
5
// (O) 멤버 변수의 값을 리턴하는 const 함수
int GetX1() const {return m_X;}
// (△) 비권장. 리턴값을 쓸데없이 const로 리턴하는 const 함수.
const int GetX2() const {return m_X;}
멤버 변수의 포인터를 리턴하는 경우, 상수 멤버 함수인지 아닌지에 따라 포인터 상수성을 맞춰서 리턴해야 합니다. 다음의 GetX4()
처럼 맞지 않게 리턴하면, 상수성 계약 위반입니다.(억지로 const_cast를 하지 마세요.)
1
2
3
4
5
6
7
8
// (O) 멤버 변수의 값을 수정하지 않는 const 함수
const int* GetX3() const {return &m_X;}
// (△) 비권장. 멤버 변수의 값을 몰래 수정할 수 있는 const 함수
int* GetX4() const {return const_cast<int*>(&m_X);}
// (O) 맴버 변수의 값을 수정하는 non-const 함수
int* GetX5() {return &m_X;}
인자(함수 선언에 작성된 Parameter)의 상수성
인수가 인자에 복사된다면, const는 무의미 하므로 사용할 필요가 없습니다.(void f(const int x);
에서 x
는 인수를 복사하므로, const는 무의미합니다. 또한 오버로딩된 함수 결정 규칙을 참고하면, 오버로딩된 함수 결정시 f(const int x)
는 f(int x)
로 취급되는걸 알 수 있습니다.)
1
2
void f(int x); // (O) 인수를 x에 복사해서 사용함.
void f(const int x); // (△) 비권장. 인수를 x에 복사해서 쓰되 f에서 수정하지 않음. 호출하는 쪽에선 무의미
인자가 포인터나 참조자 타입이라면, 인자에 const를 사용하여 전달된 인수(함수에 전달하는 개체. Argument)를 함수가 수정하는지, 수정하지 않는지 코딩 계약을 만들어 주는 게 좋습니다. 이런 정보들이 프로그래밍 환경을 좀더 쾌적하게 해주거든요.
1
2
3
4
5
void f(int* x); // (O) x가 가리키는 값을 f에서 수정함.
void f(int& x);
void f(const int* x); // (O) x가 가리키는 값을 f에서 수정하지 않음.
void f(const int& x);
상수 멤버 함수
멤버 함수의 뒤에 const를 붙여 상수 멤버 함수를 만들 수 있습니다. 상수 멤버 함수는 개체의 멤버 변수를 변경할 수 없습니다. 자세한 내용은 상수 멤버 함수를 참고 하세요.
1
2
3
4
5
6
7
8
class T {
private:
int m_Val;
public:
void Func() const { // 상수 멤버 함수입니다.
m_Val = 10; // (X) 컴파일 오류. 상수 멤버 함수에서 멤버 변수를 수정할 수 없습니다.
}
};
변경 가능 지정자(mutable)
지연 생성이나 캐쉬등 논리적 상수성인 상수 멤버 함수에서 멤버 변수를 수정해야 할 때 사용합니다. 상수 멤버 함수는 예외를 발생시키지 않습니다만, 이 경우엔 예외를 발생시킵니다. 따라서 꼭 try-catch()로 예외 처리를 해주시기 바랍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TEST(TestClassicCpp, Mutable) {
class T {
public:
// 메모리를 절약하기 위해 GetString() 으로 실제 데이터를 요청할때 문자열을 채울겁니다.
mutable std::wstring m_Lazy; // const 함수에서 수정할 수 있습니다.
// 개념적으로 내부 String을 리턴하므로 const 함수
// 하지만 내부에서 m_Lazy를 세팅하기 때문에 mutable을 사용합니다.
const std::wstring& GetString() const {
if (m_Lazy.empty()) {
m_Lazy = L"Lazy String"; // 예외가 발생할 수 있습니다.
}
return m_Lazy;
}
};
T t;
EXPECT_TRUE(t.GetString() == L"Lazy String");
}
최적화 제한 한정자(volatile)
컴파일러 최적화 옵션을 비활성 시킵니다.
디바이스에 명령을 내리기 위해, 특정 메모리 공간에 쓰기 명령을 주어야 한다고 가정해 보죠.
1
2
3
4
unsigned int *p = 0x1234;
*p = 0x0001; // 0x1234에 0x0001이 써질 때의 명령 실행
*p = 0x0002; // 0x1234에 0x0002가 써질 때의 명령 실행
*p = 0x0003; // 0x1234에 0x0003이 써질 때의 명령 실행
*p
에 쓰기 명령을 주어 3개의 명령을 수행하는 건데요, 어차피 *p
에 값을 덮어쓰다가 결국 0x0003
이 되니, 컴파일러는 최종값만 저장하는 것으로 최적화 할 수 있습니다. 다음처럼요.
1
2
unsigned int *p = 0x1234;
*p = 0x0003; // 최종값만 저장합니다. 즉, 0x1234에 0x0003이 써질 때의 명령만 실행됩니다.
최적화 때문에 *p = 0x0001;
과 *p = 0x0002;
가 무시되어 버렸는데요, 이런 최적화를 막기 위해 volatile을 사용합니다. volatile을 사용하면 컴파일러 최적화를 하지 않습니다.
1
2
3
4
volatile unsigned int *p = 0x1234; // 컴파일러 최적화를 하지 않습니다.
*p = 0x0001;
*p = 0x0002;
*p = 0x0003;
(C++20~) volatile의 일부가 deprecate되었습니다.
댓글남기기