프로그래밍/C\C++

[Effective C++ 정리 #3] const — 타입 안정성과 효율성의 시작점

허구의 2025. 6. 15. 17:42
728x90

이 글은 『Effective C++』를 읽고 개인적으로 공부한 내용을 정리한 기록입니다.

저는 컴퓨터공학을 전공하지 않았으며, 프로그래밍을 공부하는 과정에서의 이해와 생각을 정리하기 위해 글을 작성하고 있습니다.

따라서 내용 중 일부에 오류나 부정확한 설명이 있을 수 있으며, 피드백은 언제든지 환영합니다. 확인 후 수정하도록 하겠습니다.

전문적인 해설이 아닌 개인적 시선에서의 정리임을 참고하고 읽어주시면 감사하겠습니다.

Item 3: const를 붙일 수 있으면 반드시 붙여라

C++을 공부하다 보면 const가 유난히 자주 등장합니다. 처음엔 그저 상수를 뜻하는 키워드라고 생각하기 쉽지만, const는 단순한 “상수” 개념을 넘어 프로그램의 안정성과 효율성을 높이는 핵심 개념입니다. 이 글에서는 Effective C++의 Item 3에서 말하는 const의 올바른 사용법과 원리를 정리 하겠습니다.


const의 핵심 목적

const는 “이 객체는 수정되어선 안 된다”는 의미적 제약(semantic constraint)을 컴파일러에게 알려주는 장치입니다. 이 제약은 단지 문서화의 의미가 아니라, 컴파일러가 해당 객체의 변경을 강제로 막아줍니다. 그 결과 코드의 버그 가능성을 줄이고, 함수 사용자의 의도를 명확히 표현할 수 있습니다.


다양한 const 사용 예시

char greeting[] = "Hello";

// (1) 포인터도, 데이터도 변경 가능
char* p = greeting;

// (2) 포인터는 변경 가능, 데이터는 변경 불가
const char* p = greeting;

// (3) 포인터는 변경 불가, 데이터는 변경 가능
char* const p = greeting;

// (4) 둘 다 변경 불가
const char* const p = greeting;

const의 위치에 따라 의미가 달라지기 때문에 주의해야 합니다. 다음 두 함수는 의미가 완전히 동일합니다:

void f1(const Widget* pw); // 포인터가 가리키는 Widget은 const
void f2(Widget const* pw); // 위와 동일

STL Iterator에서의 const

C++의 반복자(iterator)는 포인터처럼 동작합니다. const_iteratoriterator는 const의 대상이 다릅니다:

std::vector<int> vec;

// const iterator (iterator 자체만 고정)
const std::vector<int>::iterator iter = vec.begin();
*iter = 10; // OK
++iter;     // 오류: iter는 const

// iterator to const (데이터만 고정)
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; // 오류: *cIter는 const
++cIter;     // OK

함수 선언에서의 const

const는 함수 선언에서도 유용하게 사용됩니다. 다음은 operator*const를 붙인 예시입니다:

class Rational { ... };

const Rational operator*(const Rational& lhs, const Rational& rhs);

이렇게 하면 실수로 다음과 같이 잘못된 대입을 시도하는 경우를 막을 수 있습니다:

(a * b) = c; // 컴파일 오류 발생 (operator* 결과는 const)

멤버 함수에서의 const

const 멤버 함수는 해당 함수가 객체의 상태를 변경하지 않음을 의미합니다. 이로 인해 const 객체에도 호출할 수 있으며, 다음과 같이 오버로딩도 가능합니다:

참고로 아래의 operator[]가 참조를 반환하는 이유는 str[1] = 'a'와 같이 lvalue처럼 사용하기 위함입니다. 참조가 아닌 char를 반환했다면 rvalue이기 때문에 대입으로 사용할 수 없습니다.

class TextBlock {
public:
    const char& operator[](std::size_t pos) const { return text[pos]; }
    char& operator[](std::size_t pos) { return text[pos]; }

private:
    std::string text;
};

이로 인해 const 여부에 따라 다음처럼 동작이 달라집니다:

TextBlock tb("Hello");
tb[0] = 'x'; // OK

const TextBlock ctb("World");
ctb[0] = 'x'; // 오류 발생!

bitwise vs logical const

C++ 컴파일러는 객체의 멤버 함수가 const인지 아닌지를 판단할 때, 그 함수가 객체의 비트를 실제로 수정했는지만을 기준으로 판단합니다. 이것을 bitwise const(비트 단위 불변성)이라고 합니다.

예를 들어, 다음 멤버 함수가 있다고 해봅시다:

int getValue() const {
    return cachedValue++; // <- const 함수인데 비트를 수정함!
}

이 함수는 const로 선언되어 있지만 내부에서 멤버 값을 수정하고 있기 때문에 컴파일 오류가 발생합니다. 이는 **객체의 내부 비트가 실제로 바뀌기 때문**입니다.

그러나 때때로 우리는 “논리적으로는 const지만, 내부 캐시만 갱신”하고 싶은 상황이 있습니다. 예를 들어 문자열 길이를 미리 계산해두는 캐시 같은 경우죠. 이럴 때 사용하는 것이 바로 mutable 키워드입니다.

mutable이란?

mutable은 **const 멤버 함수 내부에서도 변경할 수 있는 예외적인 멤버**를 선언할 때 사용합니다. 다시 말해, const 함수임에도 불구하고 특정 멤버 변수만은 수정 가능하게 만드는 키워드입니다.

class CTextBlock {
public:
    std::size_t length() const;

private:
    char* pText;
    mutable std::size_t textLength;   // const 함수에서도 수정 가능
    mutable bool lengthIsValid;       // 캐시 유효성
};

std::size_t CTextBlock::length() const {
    if (!lengthIsValid) {
        textLength = std::strlen(pText); // OK
        lengthIsValid = true;            // OK
    }
    return textLength;
}

이처럼 mutable을 붙인 멤버 변수는 const 함수 내에서도 수정할 수 있기 때문에, 객체 외부의 상태에는 영향을 주지 않지만 내부적으로 최적화를 수행할 수 있습니다. 이런 관점을 논리적 불변성(logical constness)이라고 부릅니다.

즉, bitwise const는 “비트가 진짜로 안 바뀌는 것”에 초점을 두고, logical const는 “외부 관점에서 보기에 상태가 바뀌지 않는 것”에 초점을 둡니다.


중복 제거를 위한 const 호출 재사용

const 멤버 함수와 non-const 멤버 함수가 동일한 구현을 갖는 경우, non-const 함수가 const 버전을 호출하면 중복 코드를 줄일 수 있습니다.

char& TextBlock::operator[](std::size_t position) {
    return const_cast<char&>(
        static_cast<const TextBlock&>(*this)[position]
    );
}

여기서는 static_castconst 객체로 만든 후, 다시 const_castchar&를 돌려받습니다. 이 방식은 “non-const → const” 호출은 안전하지만, 그 반대는 위험하므로 **절대 금지**입니다.


핵심 요약

  • 함수, 멤버, 포인터, 반복자 등 가능한 모든 곳에 적극적으로 const를 사용하길 권장합니다.
  • 컴파일러는 비트 수준의 const를 강제하지만, 의미적인 불변성(logical constness)을 사용을 권장합니다.
  • 중복된 const/non-const 함수는 const 버전을 재사용하는 구조로 구현하세요.

감사합니다.

728x90