6 분 소요

모던 C++

개요

const인 개체나 함수를 사용하면, 메모리의 수정이 없으므로 예외가 발생하지 않습니다. 예외에 안전한 프로그램을 위해, 할 수 있는 한 최대한 많이 const로 작성하는 것이 좋습니다.

또한, 상수성 계약 을 준수하세요. 계약 위반시 대부분 컴파일 오류가 발생하니, 계약을 지키기 까다롭지도 않습니다.

항목 내용
상수 개체 개체 정의시 const 한정자를 붙여 상수 개체를 만들 수 있습니다. 상수 개체는 값을 변경할 수 없습니다.
생성시 초기화 해야 합니다.
상수 멤버 함수 멤버 함수의 뒤에 const 한정자를 붙여 상수 멤버 함수를 만들 수 있습니다.
상수 멤버 함수는 개체의 멤버 변수를 변경할 수 없습니다.
단, mutable로 정의된 개체는 수정 가능합니다.

상수성 계약

상수성 계약은 값을 변경하지 않는다는 계약입니다. 메모리를 읽기만 하고 수정하지 않는다는 것이죠. 그러다 보니 예외를 발생시키지 않습니다. 논리적 상수성인 경우만 빼고요.

  1. 상수 개체는 생성과 함께 초기화 되어야 합니다.(컴파일 오류)
  2. 상수 개체는 변경할 수 없습니다.(컴파일 오류)
  3. 상수 멤버 함수멤버 변수를 수정하지 않습니다.(컴파일 오류, 단 mutable로 수정 가능합니다.)
  4. 상수 멤버 함수멤버 변수를 몰래 수정할 수 있는 포인터나 참조자를 리턴하지 않습니다.(컴파일 오류. 단, const_cast로 억지로 구현 가능하나 하지 마세요.)
  5. 상수 멤버 함수는 내부 구현에서 상수 멤버 함수만을 호출합니다.(컴파일 오류)
  6. 상수 멤버 함수예외가 발생하지 않습니다.(일반적으로 예외가 발생하지 않습니다만, 복잡한 연산들이 있다면, 노력해서 예외가 발생하지 않도록 구현해야 합니다.)
  7. 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상수 개체복사 대입시 복제본의 상수성이 제거될 수 있는 것은 참 당연한 것입니다. 복제본이니까요. 그런데, 이 당연한 규칙 때문에 복잡한 규칙들이 파생됩니다.

  1. 오버로딩된 함수 결정 규칙에서 최상위 const인자 타입에서 제거하고 취급합니다.

    예를 들어 다음의 함수를 보면 인자intconst int로 동일하지만, 둘다 int 타입의 인수나 const int 타입의 인수를 대입받을 수 있습니다. 오버로딩된 함수 결정이 모호해지므로 아예 이둘을 동일 함수로 취급합니다.

    1
    2
    
     int f(int a);
     int f(const int a);
    
  2. 함수 템플릿 인수 추론에서 최상위 const는 무시됩니다.

    템플릿 인스턴스화 할때 intconst 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되었습니다.

댓글남기기