
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 후보라고 생각해 보자
- 이 객체는 원래 const가 아닌 상태로 생성되었는가
- const std::string&처럼
단지 타입 뷰만 const로 넘어온 것인가
- const std::string&처럼
- 내가 하려는 작업은 “논리적 const”를 깨지 않는가
- 단순히 레거시 API에 맞춰 주기 위한 타입 맞추기
- 캐시 초기화처럼 외부에서 관찰되는 값은 변하지 않는 작업
- 인터페이스를 고치거나 오버로드를 추가하는 방법은
현실적인 제약 때문에 당장 쓰기 어려운가- 내가 직접 설계한 코드라면
const_cast 대신 함수 설계를 손보는 것이 우선이어야 한다
- 내가 직접 설계한 코드라면
반대로 한 줄이라도 “아닌 것 같은데” 싶은 생각이 든다면
- 캐스트 없이 해결할 방법이 없는지
- 애초에 설계에서 const를 과하게 붙인 건 아닌지
- 레거시 API를 감싸는 래퍼를 하나 더 두는 게 낫지 않은지
를 먼저 고민해 보는 편이 훨씬 안전하다
7 주의할 점
정리 차원에서
const_cast를 쓸 때 특히 조심해야 할 부분만 따로 빼 보면
- 원래 const인 객체에 쓰기 작업 금지
- 상수, 리터럴, const로 선언된 전역 변수 등
- “원래부터” const였던 애들은 const_cast로도 건드리지 않는 게 원칙
- 설계 의도 훼손
- 함수에 const를 붙여 놓고
내부에서 const_cast로 멤버를 막 수정하기 시작하면
그 순간부터 const 설계는 의미가 없어지기 시작한다
- 함수에 const를 붙여 놓고
- 멀티스레드 환경
- 캐시나 내부 상태를 const_cast로 건드리는 패턴은
동기화까지 같이 고민하지 않으면
미묘한 레이스 컨디션을 만들기 좋다
- 캐시나 내부 상태를 const_cast로 건드리는 패턴은
결국 const_cast는
“현재 타입 선언이 살짝 덜 다듬어졌을 뿐
설계 의도상으로는 괜찮은 변환이다”
라는 확신이 있을 때만
아주 좁은 범위에서 사용하는 비상구에 가깝다고 보면 된다
8 정리
마지막으로 const_cast를 기억해 둘 만한 문장만 모아 보면
- const_cast는 타입의 const / volatile 수식자를 붙이거나 떼는 캐스트다
- 메모리에 있는 실제 객체의 성격을 바꾸는 게 아니라
컴파일러가 보는 “타입 뷰”만 바꾸는 역할을 한다 - 레거시 C API와 최신 C++ 코드를 연결할 때
혹은 const 멤버 함수 내부 캐시 같은 곳에서 제한적으로 등장한다 - 원래부터 const인 객체에 const_cast로 값을 쓰기 시작하는 순간
바로 정의되지 않은 동작 영역으로 떨어질 수 있다 - 내가 직접 관리하는 코드라면
const_cast를 쓰기보다
타입 설계와 함수 인터페이스를 먼저 손보는 쪽이
장기적으로 훨씬 안전하고 읽기 좋은 코드로 이어진다
여기까지가 const_cast 편이고
다음 글에서는 dynamic_cast 쪽을 정리해 보려고 한다
'Application > C++' 카테고리의 다른 글
| [C++] 캐스트 이해하기 (4) - dynamic_cast (0) | 2025.12.11 |
|---|---|
| [C++] 캐스트 이해하기 (2) - reinterpret_cast (0) | 2025.12.09 |
| [C++] 캐스트 이해하기 (1) - static_cast (0) | 2025.12.08 |
| [C++] 숫자 쉼표(금액) 표시 하기 (Comma) (0) | 2023.11.08 |
| [C++] 2중 포인터 동적 할당 (0) | 2023.03.16 |