Application/C++

[C++] 캐스트 이해하기 (3) - const_cast

devsalix 2025. 12. 10. 11:24
728x90

1. 들어가기

static_cast랑 reinterpret_cast까지 정리해 놓고 나면
캐스트 시리즈에서 자연스럽게 다음으로 떠오르는 친구가 const_cast다

이름만 보면
“const를 없애 버리는 무서운 캐스트인가…” 싶은데
실제로는

  • 잘 쓰면 레거시 코드와 최신 코드를 이어 주는 어댑터 역할도 하고
  • 잘못 쓰면 설계 전체를 꼬이게 만드는 양날의 검 같은 존재

에 가깝다

 

이번 글에서는

  • const_cast가 정확히 무슨 일을 하는지
  • 실무에서 실제로 어디쯤에서 등장하는지
  • 절대 쓰면 안 되는 패턴은 뭐가 있는지

를 정리해 본다


2. const_cast가 하는 일

한 줄로 요약하면

타입의 const / volatile 수식자를 붙이거나 떼어 내는 캐스트

 

라고 보면 된다

 

조금 더 풀어서 말하면

  • 포인터나 레퍼런스 타입에서
  • const / volatile 부분만 바꿔 주는 캐스트

라는 것

 

예를 들어

const int* pConst = /* ... */;
int* p = const_cast<int*>(pConst);

 

이렇게 하면

  • “원래 이 값은 const였는데
  • 컴파일러한테는 지금만 잠깐 const 아닌 것처럼 보이게 해 줘”

라는 뜻이 된다

 

중요한 포인트는

const_cast는 “컴파일러가 보는 타입”만 바꿀 뿐
메모리에 있는 실제 객체의 성격은 바뀌지 않는다

 

라는 점이다

 

원래 진짜로 const로 선언된 객체를
const_cast로 강제로 바꿔 놓고 값을 써 버리면
거기서부터는 정의되지 않은 동작으로 바로 떨어질 수 있다


3. 예제 1 레거시 C API와 const 맞추기

실무에서 const_cast를 가장 자주 보게 되는 지점은
C 스타일 API와 최신 C++ 코드가 섞여 있을 때다

예를 들어 오래된 라이브러리가
아직도 이런 인터페이스를 갖고 있다고 가정해 보자

// 오래된 C API  문자열을 수정하지 않더라도 const를 안 써 둔 형태
void PrintMessage(char* msg);

 

내 코드 쪽에서는 당연히 std::string을 쓰고 있고

void Foo(const std::string& text)
{
    PrintMessage(text.c_str());  // ❌ 컴파일 오류
}

 

이렇게 바로 넘기면 컴파일이 안 된다
c_str()의 타입은 const char*인데
API는 char*을 요구하기 때문이다

이때 const_cast로 타입을 억지로 맞출 수 있다

void Foo(const std::string& text)
{
    PrintMessage(const_cast<char*>(text.c_str()));  // 가능은 함
}

 

형식상 문제는 없다


하지만 여기엔 전제가 하나 붙는다

PrintMessage가 문자열을 수정하지 않는다는 걸
100% 확신하고 있을 때만 “그나마” 허용 가능한 패턴

 

이라는 것

조금 더 안전하게 가져가려면 아예 복사본을 만들어 버리는 쪽이 낫다

void Foo(const std::string& text)
{
    // 수정될 가능성까지 염두에 둔다면
    std::vector<char> buffer(text.begin(), text.end());
    buffer.push_back('\0');

    PrintMessage(buffer.data());
}

 

혹은 내가 직접 설계하는 API라면
애초에 C 스타일이 아니라 const std::string&이나 std::string_view를 받도록
인터페이스를 고치는 게 더 정석에 가깝다


4 예제 2 const 멤버 함수 안에서의 캐시와 mutable

클래스 설계하다 보면
const 멤버 함수 안에서
결과를 캐시하고 싶은 경우가 있다

class Config
{
public:
    int GetValue() const
    {
        if (!m_initialized)
        {
            // 여기서 뭔가 계산을 한 번만 하고
            // 결과를 멤버 변수에 저장하고 싶다고 치자
        }

        return m_value;
    }

private:
    bool m_initialized = false;
    int  m_value = 0;
};

 

GetValue는 논리적으로는 “읽기 전용”이라서 const를 붙였는데
캐시를 위해 m_initialized, m_value를 수정해야 하는 상황

여기서 const_cast로 this 포인터의 const를 떼어내서
멤버를 수정하는 코드도 작성할 수는 있다

int GetValue() const
{
    auto self = const_cast<Config*>(this);  // ❌ 추천하지 않는 패턴

    if (!self->m_initialized)
    {
        self->m_value = HeavyCalc();
        self->m_initialized = true;
    }

    return self->m_value;
}

 

컴파일은 되지만
코드를 읽는 입장에서는
“const 함수인데 멤버를 몰래 바꾸네” 라는 인상을 준다

이런 패턴을 위해 C++이 따로 마련해 둔 장치가 mutable이다

class Config
{
public:
    int GetValue() const
    {
        if (!m_initialized)
        {
            m_value = HeavyCalc();
            m_initialized = true;
        }

        return m_value;
    }

private:
    mutable bool m_initialized = false;
    mutable int  m_value = 0;
};

 

이렇게 쓰면

  • 외부에서 볼 때는 여전히 “논리적 const”를 유지하면서
  • 내부 캐시 멤버만 예외적으로 수정 가능

한 상태가 된다

 

정리하자면

  • const 멤버 함수 안에서 값을 캐시하고 싶다면
    • const_cast보다는 mutable 멤버로 설계하는 쪽이
    • 의도 전달과 유지보수 측면에서 훨씬 낫다

5 예제 3 진짜 위험한 경우

const_cast가 특히 위험해지는 지점은
“원래부터 진짜 const인 객체”를 억지로 non-const처럼 쓰려고 할 때다

const int x = 10;
int* p = const_cast<int*>(&x);
*p = 20;   // ❌ 정의되지 않은 동작

 

이 코드는 컴파일은 된다
하지만 실행 결과는 컴파일러와 플랫폼에 따라 제각각일 수 있다

  • 어떤 환경에서는 값이 바뀌는 것처럼 보일 수도 있고
  • 어떤 곳에서는 아예 무시될 수도 있고
  • 최적화 과정에서 엉뚱한 동작을 할 수도 있다

비슷한 예로 문자열 리터럴도 있다

const char* msg = "hello";
char* p = const_cast<char*>(msg);
p[0] = 'H';   // ❌ 마찬가지로 정의되지 않은 동작

 

요즘 컴파일러에서는 문자열 리터럴을
읽기 전용 메모리 영역에 올려 두는 경우가 많아서
이렇게 수정하려다가 바로 크래시가 날 수도 있다

포인트는 하나다

원래 선언이 const였던 객체에
const_cast로 쓰기를 시도하는 순간부터는
결과를 아무도 보장해 주지 않는다


6 언제 const_cast를 써야 할까 체크리스트

다른 캐스트 글에서와 마찬가지로
const_cast도 “체크리스트”를 통과했을 때만 쓰는 습관이 좋다

아래 질문에 모두 예라고 답할 수 있다면
그때부터 const_cast 후보라고 생각해 보자

  1. 이 객체는 원래 const가 아닌 상태로 생성되었는가
    • const std::string&처럼
      단지 타입 뷰만 const로 넘어온 것인가
  2. 내가 하려는 작업은 “논리적 const”를 깨지 않는가
    • 단순히 레거시 API에 맞춰 주기 위한 타입 맞추기
    • 캐시 초기화처럼 외부에서 관찰되는 값은 변하지 않는 작업
  3. 인터페이스를 고치거나 오버로드를 추가하는 방법은
    현실적인 제약 때문에 당장 쓰기 어려운가
    • 내가 직접 설계한 코드라면
      const_cast 대신 함수 설계를 손보는 것이 우선이어야 한다

반대로 한 줄이라도 “아닌 것 같은데” 싶은 생각이 든다면

  • 캐스트 없이 해결할 방법이 없는지
  • 애초에 설계에서 const를 과하게 붙인 건 아닌지
  • 레거시 API를 감싸는 래퍼를 하나 더 두는 게 낫지 않은지

를 먼저 고민해 보는 편이 훨씬 안전하다


7 주의할 점

정리 차원에서
const_cast를 쓸 때 특히 조심해야 할 부분만 따로 빼 보면

  • 원래 const인 객체에 쓰기 작업 금지
    • 상수, 리터럴, const로 선언된 전역 변수 등
    • “원래부터” const였던 애들은 const_cast로도 건드리지 않는 게 원칙
  • 설계 의도 훼손
    • 함수에 const를 붙여 놓고
      내부에서 const_cast로 멤버를 막 수정하기 시작하면
      그 순간부터 const 설계는 의미가 없어지기 시작한다
  • 멀티스레드 환경
    • 캐시나 내부 상태를 const_cast로 건드리는 패턴은
      동기화까지 같이 고민하지 않으면
      미묘한 레이스 컨디션을 만들기 좋다

결국 const_cast는

“현재 타입 선언이 살짝 덜 다듬어졌을 뿐
설계 의도상으로는 괜찮은 변환이다”

 

라는 확신이 있을 때만
아주 좁은 범위에서 사용하는 비상구에 가깝다고 보면 된다


8 정리

마지막으로 const_cast를 기억해 둘 만한 문장만 모아 보면

  • const_cast는 타입의 const / volatile 수식자를 붙이거나 떼는 캐스트다
  • 메모리에 있는 실제 객체의 성격을 바꾸는 게 아니라
    컴파일러가 보는 “타입 뷰”만 바꾸는 역할을 한다
  • 레거시 C API와 최신 C++ 코드를 연결할 때
    혹은 const 멤버 함수 내부 캐시 같은 곳에서 제한적으로 등장한다
  • 원래부터 const인 객체에 const_cast로 값을 쓰기 시작하는 순간
    바로 정의되지 않은 동작 영역으로 떨어질 수 있다
  • 내가 직접 관리하는 코드라면
    const_cast를 쓰기보다
    타입 설계와 함수 인터페이스를 먼저 손보는 쪽이
    장기적으로 훨씬 안전하고 읽기 좋은 코드로 이어진다

여기까지가 const_cast 편이고
다음 글에서는 dynamic_cast 쪽을 정리해 보려고 한다


 

728x90
반응형