Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

3. 낌새만 보이면 const를 들이대 보자! 본문

책/Effective C++

3. 낌새만 보이면 const를 들이대 보자!

아롱로또 2023. 10. 31. 02:46

작성 이유

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