C++ const 에러: passing as const 컴파일 에러 원인과 const 정합성
이 글의 핵심
const를 어디에 붙여야 할지 모른 채 에러가 날 때마다 const를 떼어 내면 문제가 다른 곳으로 옮겨갈 뿐입니다. const 객체에서는 const 멤버 함수만 호출할 수 있다는 규칙 하나가 대부분의 에러를 설명하고, 읽기 전용 인자를 const 참조로 받는 습관이 연쇄 에러를 막습니다. 에러 메시지별 수정 방법과 const_cast를 피해야 하는 이유까지 정리했습니다.
들어가며: “passing as const 에러가 계속 나요”
C++에서 const는 타입 안전성을 높이는 핵심 키워드이지만, const 관련 에러는 초보자가 가장 자주 겪는 컴파일 에러 중 하나입니다.
// ❌ 에러 코드
void print(std::string& s) { // 비const 참조
std::cout << s << '\n';
}
int main() {
print("Hello"); // 임시 객체 → const 참조만 가능
}
// error: cannot bind non-const lvalue reference of type 'std::string&'
// to an rvalue of type 'std::string'
이 글에서 다루는 것:
- const 관련 에러 10가지 패턴
- 10가지 에러 뒤에 있는 규칙 네 가지 (개념 설명은 C++ const 정확성에서)
- GCC·Clang·MSVC 에러 메시지 대응표와 템플릿 안에서 난 에러 읽는 법
자주 나오는 const 에러 10가지
에러 1: passing as const (가장 흔함)
// ❌ 에러 코드
void modify(std::string& s) { // 비const 참조
s += " world";
}
int main() {
modify("Hello"); // 임시 객체는 비const 참조에 바인딩 불가
}
// error: cannot bind non-const lvalue reference of type 'std::string&'
// to an rvalue of type 'std::string'
해결:
// ✅ const 참조로 변경
void print(const std::string& s) { // const 참조
std::cout << s << '\n';
}
int main() {
print("Hello"); // OK
}
위 해결은 함수가 읽기만 하는 경우에 해당합니다. modify처럼 실제로 문자열을 바꾸는 함수라면 매개변수를 const로 바꿀 수 없으므로 방향이 달라집니다. 호출하는 쪽에서 std::string s = "Hello"; modify(s);처럼 이름 있는 변수를 넘기거나, 함수를 std::string modify(std::string s)처럼 값으로 받아 수정한 결과를 돌려주도록 바꿉니다. 임시 객체는 그 줄이 끝나면 사라지는 값이라, 거기에 한 수정은 어차피 아무도 볼 수 없습니다. 언어가 비const 참조 바인딩을 막는 이유가 바로 이 “사라질 값을 수정하는 실수”를 컴파일 타임에 잡기 위해서입니다. 참고로 MSVC는 예전부터 이 바인딩을 확장으로 허용해 와서, Windows에서는 컴파일되던 코드가 GCC·Clang이나 /permissive- 옵션에서 갑자기 에러가 나는 일이 흔합니다.
에러 2: discards qualifiers
// ❌ 에러 코드
class MyClass {
int value_;
public:
int getValue() { // const 없음
return value_;
}
};
void print(const MyClass& obj) {
std::cout << obj.getValue() << '\n'; // const 객체에서 비const 함수 호출
}
// error: passing 'const MyClass' as 'this' argument discards qualifiers
해결:
// ✅ const 멤버 함수
class MyClass {
int value_;
public:
int getValue() const { // const 추가
return value_;
}
};
에러 3: assignment of read-only variable
// ❌ 에러 코드
void foo() {
const int x = 42;
x = 99; // const 변수 수정
}
// error: assignment of read-only variable 'x'
해결: const를 제거하거나, 새 변수 사용.
// ✅ const 제거
void foo() {
int x = 42;
x = 99; // OK
}
에러 4: cannot convert const to non-const
// ❌ 에러 코드
void modify(int* ptr) { // 비const 포인터
*ptr = 99;
}
int main() {
const int x = 42;
modify(&x); // const int* → int* 변환 불가
}
// error: invalid conversion from 'const int*' to 'int*'
해결:
// ✅ const 포인터로 변경
void print(const int* ptr) { // const 포인터
std::cout << *ptr << '\n';
}
int main() {
const int x = 42;
print(&x); // OK
}
에러 5: const 멤버 함수에서 멤버 수정
// ❌ 에러 코드
class Counter {
int count_;
public:
void increment() const { // const 멤버 함수
++count_; // const 함수에서 멤버 수정 불가
}
};
// error: increment of member 'Counter::count_' in read-only object
해결:
// ✅ 해결 1: const 제거
void increment() { // 비const 함수
++count_;
}
// ✅ 해결 2: mutable 사용 (호출 횟수 통계처럼 객체의 "값"이 아닌 부가 정보일 때만)
class Counter {
mutable int count_; // mutable: const 함수에서도 수정 가능
public:
void increment() const {
++count_; // OK
}
};
이 예제에서 대부분은 해결 1이 맞습니다. increment는 이름 그대로 객체의 상태를 바꾸는 함수이므로 const일 이유가 없습니다. mutable은 “바깥에서 관찰되는 값은 그대로인데 내부 구현상 바뀌어야 하는 것”, 즉 계산 결과 캐시·뮤텍스·조회 통계 같은 곳에만 씁니다. 에러를 없애려고 mutable을 붙이기 시작하면 const 함수가 실제로는 상태를 바꾸는 코드가 되어 const의 의미가 사라집니다.
한 가지 더 주의할 점은 스레드 안전성입니다. 표준 라이브러리는 const 멤버 함수를 여러 스레드에서 동시에 호출해도 안전하다고 가정합니다. 그런데 mutable 멤버를 const 함수 안에서 동기화 없이 수정하면, 두 스레드가 같은 const 객체의 increment()를 호출하는 순간 데이터 경쟁이 생깁니다. 그래서 mutable 캐시에는 mutable std::mutex나 std::atomic을 함께 두는 것이 관례입니다.
에러 6: 반환 타입 const 불일치
// ❌ 에러 코드
class MyClass {
std::string name_;
public:
std::string& getName() const { // const 함수가 비const 참조 반환
return name_; // const 멤버를 비const 참조로 반환 불가
}
};
// error: binding reference of type 'std::string&' to 'const std::string'
// discards qualifiers
해결:
// ✅ const 참조 반환
class MyClass {
std::string name_;
public:
const std::string& getName() const { // const 참조 반환
return name_;
}
};
에러 7: const 객체에서 비const 반복자
// ❌ 에러 코드
void print(const std::vector<int>& vec) {
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) { // const 벡터의 begin()은 const_iterator
std::cout << *it << '\n';
}
}
// error: conversion from '__normal_iterator<const int*, ...>' to non-scalar type '__normal_iterator<int*, ...>' requested
auto it = vec.begin()이라면 auto가 const_iterator로 추론되어 에러가 나지 않습니다. 이 에러는 반복자 타입을 iterator로 직접 적었을 때, 또는 iterator를 받는 함수에 넘길 때 납니다.
해결:
// ✅ const_iterator 사용
void print(const std::vector<int>& vec) {
for (auto it = vec.cbegin(); it != vec.cend(); ++it) { // cbegin/cend
std::cout << *it << '\n';
}
}
// ✅ 또는 범위 기반 for
void print(const std::vector<int>& vec) {
for (int x : vec) {
std::cout << x << '\n';
}
}
에러 8: const 오버로드 불일치
// ❌ 에러 코드
class MyClass {
std::vector<int> data_;
public:
int& operator[](size_t idx) { // 비const 버전만
return data_[idx];
}
};
void print(const MyClass& obj) {
std::cout << obj[0] << '\n'; // const 객체에서 비const operator[] 호출
}
// error: passing 'const MyClass' as 'this' argument discards qualifiers
해결:
// ✅ const 오버로드 추가
class MyClass {
std::vector<int> data_;
public:
int& operator[](size_t idx) { // 비const 버전
return data_[idx];
}
const int& operator[](size_t idx) const { // const 버전
return data_[idx];
}
};
에러 9: const 반복자 수정
// ❌ 에러 코드
void modify(const std::vector<int>& vec) {
for (auto it = vec.begin(); it != vec.end(); ++it) {
*it = 99; // const_iterator는 수정 불가
}
}
// error: assignment of read-only location
해결: 매개변수를 비const로 변경.
// ✅ 비const 참조
void modify(std::vector<int>& vec) { // const 제거
for (auto& x : vec) {
x = 99; // OK
}
}
에러 10: const 멤버 초기화 실수
// ❌ 에러 코드
class MyClass {
const int value_;
public:
MyClass() {
value_ = 42; // const 멤버는 대입 불가
}
};
// error: assignment of read-only member 'MyClass::value_'
해결:
// ✅ 초기화 리스트 사용
class MyClass {
const int value_;
public:
MyClass() : value_(42) { // 초기화 리스트
// ...
}
};
에러 뒤에 있는 규칙 네 가지
위 10가지 에러는 모두 아래 규칙 중 하나에서 나옵니다. const 참조와 값 전달의 선택, const 멤버 함수와 오버로드, 포인터에서 const 위치 읽는 법, mutable과 const_cast의 정확한 의미는 C++ const 정확성에서 예제와 함께 설명했으므로, 여기서는 에러를 읽을 때 필요한 만큼만 요약합니다.
- 비const lvalue 참조(
T&)는 임시 객체와 const 객체에 바인딩되지 않습니다. 에러 1·4가 이것입니다. 읽기만 하는 매개변수라면const T&로 바꾸고, 정말 수정해야 한다면 호출하는 쪽이 임시 객체가 아닌 변수를 넘겨야 합니다. - const 객체(또는 const 참조·포인터로 접근한 객체)에서는 const 멤버 함수만 호출할 수 있습니다. 에러 2·8과 “discards qualifiers”류 에러 전부입니다. 멤버 함수 뒤의
const는this의 타입을const T*로 바꾸는 표시이기 때문에, 비const 함수에 const 객체를 넘기면this에서 const를 “버리는(discard)” 변환이 필요해집니다. 에러 메시지의 “as ‘this’ argument”가 이 뜻입니다. - const 멤버 함수 안에서는 멤버도 const다. 에러 5·6이 여기서 나옵니다. 멤버를 수정하거나 비const 참조로 내보낼 수 없습니다. 외부에서 관찰되는 상태가 바뀌지 않는 캐시·뮤텍스만
mutable로 예외를 둡니다. - 포인터의 const는
*를 기준으로 왼쪽이면 대상, 오른쪽이면 포인터 자체.const int*는 가리키는 값을 못 바꾸고,int* const는 포인터를 다른 곳으로 못 돌립니다. 에러 4의const int*→int*변환이 막히는 이유입니다.
const_cast로 에러를 없앨 수는 있지만, 원래 const로 선언된 객체를 그렇게 수정하면 정의되지 않은 동작입니다. 컴파일러가 그 값을 상수로 접어 두었을 수 있어서 “수정했는데 출력은 옛 값”인 현상이 실제로 나타납니다. 쓸 수 있는 경우는 const를 받지 않는 오래된 C API에 “실제로는 수정하지 않는” 포인터를 넘길 때 정도입니다.
에러가 나지 않아서 더 위험한 const 함정 두 가지
컴파일 에러는 적어도 문제를 알려 주지만, const가 조용히 의도와 다르게 동작하는 경우도 있습니다.
첫째, const는 포인터 멤버를 따라 전파되지 않습니다. const 멤버 함수 안에서 int* data_는 int* const가 될 뿐, 가리키는 대상은 여전히 수정할 수 있습니다. 그래서 void reset() const { *data_ = 0; }은 에러 없이 컴파일되고, const 객체의 내용이 바뀝니다. std::unique_ptr·std::shared_ptr 멤버도 마찬가지입니다. 이런 “얕은 const”를 막으려면 const 버전과 비const 버전의 접근자를 따로 두거나, 실험적 라이브러리의 propagate_const 같은 래퍼를 씁니다.
둘째, const 객체에 std::move를 쓰면 이동이 아니라 복사가 됩니다. const std::string s = ...; v.push_back(std::move(s));에서 std::move(s)의 타입은 const std::string&&인데, 이동 생성자는 std::string&&만 받으므로 복사 생성자가 선택됩니다. 에러도 경고도 없이 성능만 떨어집니다. 같은 이유로 const std::string getName() const처럼 값 반환 타입에 const를 붙이면 호출 쪽에서 이동을 받을 수 없으므로, 값으로 반환할 때는 const를 붙이지 않습니다. 이동과 const의 관계는 C++ 이동 의미론에서 더 자세히 다룹니다.
컴파일러별 에러 메시지 읽기
같은 규칙 위반이라도 컴파일러마다 메시지가 다르고, 템플릿 안에서 나면 첫 줄이 원인과 멀리 떨어진 곳을 가리킵니다.
| 규칙 위반 | GCC | Clang | MSVC |
|---|---|---|---|
| const 객체에서 비const 멤버 호출 | passing 'const X' as 'this' argument discards qualifiers | 'this' argument to member function 'f' has type 'const X', but function is not marked const | C2662 cannot convert 'this' pointer from 'const X' to 'X &' |
임시 객체를 T&에 바인딩 | cannot bind non-const lvalue reference of type 'T&' to an rvalue | non-const lvalue reference to type 'T' cannot bind to a temporary | C2664 (인자 변환 실패) |
| const 변수·멤버에 대입 | assignment of read-only variable/member | cannot assign to variable 'x' with const-qualified type | C3892, C2166 |
const T* → T* | invalid conversion from 'const T*' to 'T*' | cannot initialize a parameter of type 'T *' with an rvalue of type 'const T *' | C2440 |
Clang 메시지가 대체로 가장 직접적이라, GCC 메시지가 이해되지 않을 때 같은 코드를 Clang으로 컴파일해 보는 것도 방법입니다(Compiler Explorer로 쉽게 비교할 수 있습니다).
템플릿 안에서 나는 경우: std::sort(v.begin(), v.end())에 const 벡터를 넘기면(실전 사례 1) 에러는 표준 라이브러리 헤더 깊숙한 곳의 *it = ... 대입에서 납니다. 메시지 첫 줄이 stl_algo.h 같은 파일을 가리키면, 아래쪽의 “required from here” 또는 “in instantiation of” 줄을 따라 내 코드의 줄을 먼저 찾습니다. 거기서 넘긴 인자의 타입이 const인지 보면 대부분 원인이 드러납니다.
const를 떼어서 해결하지 않기: 에러가 날 때마다 호출된 쪽이 아니라 호출하는 쪽의 const를 떼면, 에러는 사라지지만 const가 아닌 객체가 점점 코드 전체로 번집니다. 에러의 수정 방향은 거의 항상 호출되는 함수에 const를 붙이는 쪽(매개변수를 const T&로, 멤버 함수에 const를)입니다. 반대 방향이 맞는 경우는 함수가 실제로 객체를 수정하는 경우(에러 9)뿐입니다.
실전 사례 분석
사례 1: STL 알고리즘 const 에러
에러 코드:
// ❌ 에러 코드
void sortData(const std::vector<int>& data) {
std::sort(data.begin(), data.end()); // const vector는 정렬 불가
}
// GCC: stl_algo.h 안쪽에서 error: assignment of read-only location ...
// (아래 "required from here" 줄이 sortData의 호출 위치를 가리킴)
해결:
// ✅ 비const 참조
void sortData(std::vector<int>& data) { // const 제거
std::sort(data.begin(), data.end());
}
// ✅ 또는 복사본 정렬
std::vector<int> sortData(const std::vector<int>& data) {
std::vector<int> sorted = data; // 복사
std::sort(sorted.begin(), sorted.end());
return sorted;
}
사례 2: getter/setter const 불일치
에러 코드:
// ❌ 에러 코드
class Person {
std::string name_;
public:
std::string& getName() { // 비const 함수
return name_;
}
};
void print(const Person& p) {
std::cout << p.getName() << '\n'; // const 객체에서 비const 함수 호출
}
// error: passing 'const Person' as 'this' argument discards qualifiers
해결:
// ✅ const 오버로드
class Person {
std::string name_;
public:
std::string& getName() { // 비const 버전
return name_;
}
const std::string& getName() const { // const 버전
return name_;
}
};
const correctness 체크리스트
함수 매개변수
- 읽기만 하는 매개변수는 const 참조인가?
- 작은 타입 (int, double)은 값 전달인가?
- 수정하는 매개변수는 비const 참조인가?
멤버 함수
- 멤버 변수를 수정하지 않는 함수는 const인가?
- getter는 const 멤버 함수인가?
- const 오버로드가 필요한가? (operator[], at 등)
반환 타입
- const 멤버 함수는 const 참조를 반환하는가?
- 임시 객체를 반환하는가? (const 참조 금지)
기존 코드에 const를 뒤늦게 붙일 때
기존 코드베이스에 const를 뒤늦게 도입하면 한 함수에 const를 붙일 때마다 그 함수가 부르는 함수들에서 에러가 연쇄적으로 터집니다. const가 호출 경로를 따라 전염되기 때문에 생기는 자연스러운 과정이므로, 가장 아래쪽(다른 함수를 부르지 않는 getter·유틸리티)부터 const를 붙여 위로 올라가면 연쇄 에러를 훨씬 적게 겪습니다.
const를 성능 최적화 수단으로 기대하지 않는 것도 중요합니다. const 참조가 가리키는 객체도 다른 경로(별칭)로 바뀔 수 있어서 컴파일러가 값이 그대로라고 가정하기 어렵고, const가 최적화에 주는 도움은 생각보다 제한적입니다. const의 실제 가치는 의도하지 않은 수정을 컴파일 타임에 막고 함수가 무엇을 바꾸는지 시그니처로 드러내는 데 있습니다.
같이 보면 좋은 글
자주 묻는 질문 (FAQ)
Q. const map에서 operator[]를 쓰면 왜 컴파일 에러가 나나요?
A. std::map::operator[]는 키가 없으면 기본값으로 원소를 삽입하는 함수라서 non-const 멤버 함수로만 제공됩니다. 그래서 const 객체나 const std::map& 매개변수로 받은 map에서 호출하면 discards qualifiers 계열의 에러가 납니다. 조회만 할 때는 find()로 반복자를 받아 존재 여부를 확인하거나, 키가 반드시 있어야 한다면 없을 때 std::out_of_range를 던지는 at()을 쓰면 됩니다.