본문 바로가기

진리는어디에/C++

[C++] const 와 mutable 키워드

이번 포스트는 변수를 상수화 시켜 코드에서 변수의 값을 변경하려고 할 경우 컴파일 타임에 에러를 발생 시키는 const 키워드와 const를 무효화 시킬 수 있는 mutable 키워드에 대해 살펴 보도록 하겠다.

C++에서는 절대 변경 되어서는 안되는 변수(변수라는 것 자체가 변경을 할 수 있다는 의미인데 그것을 변경하지 못하게 막겠다는게 개념적으로 아이러니 하긴하지만)에 const라는 한정자를 더해, 변수를 변경하려고 시도 할 경우 컴파일 타임에 감지하여 에러를 발생 시킨다.

const 이전에는 #define 전처리 명령어를 이용했지만 전처리 명령어를 이용하는 것은 스코프를 한정 할 수도 없고, 타입에 대한 제약도 없어 여러 모로 실수를 만들 요소를 가지고 있다. 하여 const 키워드 이후 부터는 상수 변수가 필요한 경우 #define 대신 const 한정자를 사용하는것을 권고한다.

이번 포스트에서는 const에 대해서 알아 보도록 하자.

변수에서 const 사용

1. 일반 변수

char str[] = "Hello World";
char* greeting = str;

아무런 제약이 없는 문자열의 선언이다. 포인터를 변경할 수도 있고, 포인터가 가리키고 있는 값 역시 변경이 가능하다.

2. 비상수 포인터, 상수 데이터

const char * greeting = str;
// or
char const * greeting = str;

비상수 포인터, 상수 데이터는 변수가 가리키고 있는 포인터는 변경이 가능하지만, 포인터가 가리키고 있는 값은 변경이 불가능하다는 의미다. const 키워드가 '*' 의 왼쪽에 위치하며, 'const char *'와 'char const *'는 동일한 의미를 가진다. 이해하기 쉽게 const 키워드의 오른쪽에 있는 '*greeting', 즉, 포인터가 가리키는 값이 const라고 생각하자.

char str1[] = "hello world";
char str2[] = "I'm fine";
	
const char * greeting = str1;

greeting = str2; // 포인터를 변경하는 것은 OK
greeting[0] = 'H'; // 포인터가 가리키고 있는 값을 변경 하려고 한다. ERROR!!

3. 상수 포인터, 비상수 데이터

char * const greeting = str;

const 키워드가 '*'의 오른쪽에 위치한다. const greeting 라고 생각하라. 여기서 greeting은 문자열 포인터를 가리키고 있는 포인터 변수다. 포인터가 변경 되는 것을 방지하지만, 포인터가 가리키는 값은 변경이 가능하다.

char str1[] = "hello world";
char str2[] = "I'm fine";
	
char * const greeting = str1;

greeting = str2; // 포인터를 변경하려고 시도한다. ERROR
greeting[0] = 'H'; // 값을 변경하려고 시도한다. OK

4. 상수 포인터, 상수 데이터

const char* const greeting = str;

이런식으로 const 키워드가 char*와 greeting 앞에 모두 붙게 되면, 포인터든, 포인터가 가리키고 있는 값이든 둘 다 변경 될 수 없다는 것을 의미한다.

char str1[] = "hello world";
char str2[] = "I'm fine";
	
const char * const greeting = str1;

greeting = str2; // 포인터를 변경 하려고 한다. ERROR
greeting[0] = 'H'; // 포인터가 가리키고 있는 값을 변경하려고 한다. ERROR

함수에서 const 사용

1. 리턴값의 상수화

class TextBlock 
{
public :
    const char& operator[](std::size_t position) const 
    {
        return m_text[position];
    }
    
    char& operator[](std::size_t position) 
    {
        return m_text[position];
    }
private :
    std::string m_text;
};

위의 예제를 보도록 하자. 일단 가장 눈에 먼저 들어오는 것은 같은 이름, 같은 파라메터함수의 오버로딩이다. 하지만 const 함수와 비 const 함수 사이에는 이런식으로 오버라이딩이 가능하다

그리고 호출 될때의 구분은 아래와 같이 대입 되는 변수가 const인지 아닌지에 따라 구분 된다.

void print(const TextBlock& ctb) 
{
    std::cout << ctb[0];
}

void print(TextBlock& tb) 
{
    std::cout << tb[0];
}

리턴값을 const로 하는 가장큰 이유는 참조자 리턴 때문이다. 일반 변수를 복사하여 리턴하게 된다면 클래스 내부의 멤버변수(혹은 함수를 통해 리턴되는 전역변수)의 값을 변화 시킬 수 있는 방법은 없다. 하지만 큰 객체를 리턴하는 경우 복사 비용이 발생하게 된다. 하여 참조자를 리턴하게 된다면 복사 비용은 줄일 수 있지만 원본 변수에 접근할 수 있는 방법이 생기게 되고, 이를 이용해 자신의 의도와는 상관없이 값을 바꾸는 경우가 발생할 수 있다. 원래 원했던 의도라고 한다면 상관 없지만 const는 그러면 안되는 경우를 위해 있는 것이다.

이런 경우 const키워드를 이용해 참조자를 리턴하게 된다면 리턴된 값을 바꾸려는 시도를 애초에 원천적으로 차단할 수 있다.

2. 함수의 상수화

위의 클래스 예제에서 오퍼레이터 오버라이딩 선언 뒤에 const 키워드가 붙는 것을 볼 수 있다.

const char& operator[](std::size_t position) const {
    return m_text[position];
}

이것은 함수의 정의가 상수화 된다는 뜻으로 함수 내에서 read 연산(출력 같은 것들)은 가능하지만, write 연산('='같은 값을 변경하는 대입 연산)은 허용되지 않는다. const가 붙은 함수 안에서는 절대적으로 모든 값들이 자신의 값 그대로를 유지한다는 보장을 해 줄 수 있다.

mutable

이야기를 여기까지 끌고 왔는데 제일 처음 제목으로 나왔던 mutable이라는 것에 대해서는 아직까지 한번도 언급이 없었다. 지금까지 const의 기능에 대해 이야기 했다면 이제 부터는 mutable에 대해서 이야기 하겠다.

거창하게 이야기 했지만 mutable은 그렇게 대단하거나 복잡한 것이 아니다. 다만 const를 무효화 시킬수 있는 키워드 정도라고 이해하면 된다. 위에서 const 키워드가 붙은 함수 내에서는 어떤 변수에도 write 연산을 할 수 없다고 했었지만 mutable 키워드를 사용하게 되면 이야기가 달라진다.

위의 예제에 [] 연산자 오버로딩으로 char 참조자를 리턴하도록 되어있는 const함수에 가장 최근에 리턴된 문자가 무엇인지 기록할수 있는 기능을 추가 해보도록 하자.

class TextBlock 
{
public :
    const char& operator[](std::size_t position) const 
    {
        m_recentReturnVal = m_text[position];
        return m_recentReturnVal;
    }
    
    char& operator[](std::size_t position) 
    {
        return m_text[position];
    }
private :
    std::string m_text;
    char m_recentReturnVal;  
};

위 코드를 컴파일하게 되면 6라인에서 컴파일 에러가 발생한다. const로 선언된 함수 내에서 왜 wirte 연산을 하려 하냐고 컴파일러가 따진다. 똑똑한놈이다. 나름 아무도 모르게 한다고 했는데 했는데 어느새 눈치 채고 지적질이다.

하지만 변수를 선언 할 때 mutable이라는 키워드를 쓴다면 컴파일러의 에러를 잠재울 수 있다.

class TextBlock 
{
public :
    const char& operator[](std::size_t position) const 
    {
        m_recentReturnVal = m_text[position];
        return m_recentReturnVal;
    }
    
    char& operator[](std::size_t position) 
    {
        return m_text[position];
    }
private :
    std::string m_text;
    mutable char m_recentReturnVal;  
};

위와 같이 mutable 키워드가 붙은 볌수는 const 함수 내에서라도 변경이 가능하도록 예외 처리 할 수 있다.

마치며

이상 변수의 변경을 컴파일 타임에 막을 수 있는 const 키워드와 그것을 무효화 할 수 있는 mutable 키워드에 대해 살펴 보앗다. 하지만 어떤 경우에 mutable을 써야 하는가라는 의문이 남는다. const라고 엄연히 규칙을 정해 놓고 mutable로 그 규칙을 마음대로 위반한다면 const 함수라는 이름 자체가 무색해져 버린다.

이 글에서 명확한 답을 주면 좋겠지만, C++은 이런 저런 다양한 도구들을 프로그래머 앞에 깔아 놓고, 알아서 골라 쓰세요, 다만 책임은 여러분이 집니다라는 성향이 강한 언어라 나 또한 여러분에게 드릴 수 있는 조언이 거기까지다.

const를 사용하는 부분에서는 mutable을 쓰지마세요라고 말하고 싶지만 프로그래밍을 하다 보면 워낙 상황이 다양해서 무조건 쓰지 마세요라고는 하지 못하겠다. mutable을 남발하지말고 최대한 실수을 안 할수 있는 범위 내에서 사용하도록 하자.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!