이 코드는 틀렸다.
std::string s1, s2;
const char* p = (s1 + s2).c_str(); // p는 이미 죽은 메모리를 가리킨다s1 + s2가 임시 std::string을 만들고, c_str()이 그 안을 가리키는 포인터를 준다. 그리고 세미콜론에서 임시가 소멸한다. 다음 줄부터 p는 남의 땅을 가리킨다.
규칙 자체는 한 줄이다. 임시 객체는 자기가 태어난 **전체 표현식(full-expression)**의 평가가 끝나는 마지막 단계에 소멸한다. 위 줄에서 전체 표현식은 대입문 전체이므로, 세미콜론을 만나는 순간 임시는 없다.
문제는 이 코드가 경고 없이 컴파일되고, 실행해도 죽지 않는다는 것이다.
죽지 않는다. 조용히 틀린 값을 준다
int main() {
std::string s1(40,'a'), s2(40,'b');
const char* p = (s1 + s2).c_str();
std::string filler(40,'Z'); // 방금 해제된 자리를 차지할 수 있다
std::printf("p = %.10s\n", p);
}p = ZZZZZZZZZZ
aaaaaaaaaa가 나와야 할 자리에 Z가 나온다. 임시가 쓰던 힙 버퍼가 해제됐고, 바로 뒤에 만든 filler가 같은 자리를 받아 자기 데이터로 덮었다. p는 그걸 읽었다.
크래시가 아니라 오염이다. 세그폴트라면 차라리 낫다 — 바로 알 수 있으니까. 이건 그냥 다른 값이 나오고, 운이 좋으면(혹은 나쁘면) 테스트에서는 원래 값이 그대로 나오기도 한다. 할당자가 그 자리를 아직 안 줬을 뿐이다.
컴파일러는 어디까지 잡아주나
이게 이 글의 핵심이다. 임시 수명 문제는 다 같아 보이지만 도구의 대응은 셋으로 갈린다. g++ 13.2로 확인했다.
1. c_str() 댕글링 — 아무도 안 잡는다
const char* p = (s1 + s2).c_str();g++ -std=c++20 -Wall -Wextra -fanalyzer -O2 -c d.cpp아무 경고도 없다. -Wall도, -Wextra도, 정적 분석기(-fanalyzer)까지 켜도 조용하다.
이유를 짐작하면 이렇다. c_str()은 그냥 const char*를 반환하는 함수다. 반환된 포인터가 객체 내부를 가리키는지, 그 객체가 임시인지 — 컴파일러가 알려면 c_str()의 의미를 특별히 알아야 한다. 타입 시스템에는 그 정보가 없다.
즉 이 케이스는 여전히 사람이 지켜야 한다.
2. 임시에 묶인 참조를 반환 — 경고, 그리고 C++26에서는 에러
const std::string& f() {
return std::string("temp");
}warning: returning reference to temporary [-Wreturn-local-addr]
-Wall만으로 잡힌다. 그리고 이건 곧 경고가 아니게 된다. C++26의 P2748R5가 “참조를 반환하는 함수에서 반환 참조를 임시 표현식에 묶는 return 문은 ill-formed”로 못 박았다. 경고가 아니라 컴파일 에러가 된다는 뜻이다.
P2748R5는 표준 문서 기준이다. g++ 13.2는
-std=c++26을 아직 모르므로 이 글에서 직접 확인하지는 못했다.
3. 임시의 멤버 함수가 준 참조 — GCC 13부터 잡는다
이게 가장 미묘한 케이스다. 임시에 참조를 묶으면 수명이 연장되는 규칙이 있는데, 어디까지 연장되는지가 갈린다.
struct Person { std::string name; };
struct DiceRoll {
std::string nick;
const std::string& nickname() const { return nick; }
};
const std::string& a = GeneratePerson().name; // 안전
const std::string& b = GenerateDiceRoll().nickname(); // 위험.name은 안전하다. 임시의 하위 객체에 직접 묶는 것이라, 컴파일러가 “이 참조가 임시 안의 무언가를 붙잡고 있다”는 걸 문법으로 안다. 그래서 임시 전체를 참조의 수명만큼 살려둔다.
.nickname()은 위험하다. 반환값은 그냥 const std::string&이다. 그게 임시의 멤버인지, 전역인지, 어딘가 다른 곳인지 호출부의 타입만 봐서는 알 수 없다. 수명 연장 규칙이 적용되지 않고, 임시는 세미콜론에서 죽는다.
원문 팁이 이 대목을 “위험할 정도로 미묘하다”고 표현하는데, 지금은 컴파일러가 잡는다.
warning: possibly dangling reference to a temporary [-Wdangling-reference]
note: the temporary was destroyed at the end of the full expression
'GenerateDiceRoll()().DiceRoll::nickname()'
-Wdangling-reference는 GCC 13에 들어왔고 -Wextra에 포함된다. 팁이 마지막으로 갱신된 게 2020년이니 그때는 없던 도구다.
정리하면
| 케이스 | 도구 |
|---|---|
(s1 + s2).c_str() | 없음. -Wall/-Wextra/-fanalyzer 모두 침묵 |
| 임시에 묶인 참조를 반환 | -Wreturn-local-addr (-Wall) → C++26에서 에러 |
임시().멤버함수()의 참조 | -Wdangling-reference (-Wextra, GCC 13+) |
-Wextra를 켜라. 셋 중 하나는 그걸로 잡힌다. 그리고 나머지 하나는 여전히 당신 몫이다.
어떻게 피하나
전체 표현식 안에서 다 쓴다
임시는 세미콜론까지는 살아 있다. 그 안에서 끝내면 된다.
size_t len = strlen((s1 + s2).c_str()); // 안전strlen이 반환될 때까지 임시는 멀쩡하다. 다만 이렇게 쓸 수 있는 경우가 많지는 않다.
그냥 변수에 담는다
가장 단순하고, 대개 정답이다.
std::string tmp = s1 + s2;
const char* p = tmp.c_str(); // 안전“복사가 생기지 않나?” 싶겠지만 안 생긴다. 임시는 tmp가 될 자리에 바로 만들어진다. C++17부터는 이게 최적화가 아니라 표준이 보장하는 동작이다.
참조로 받는 건 권하지 않는다
const std::string& tmp = s1 + s2; // 동작은 한다수명 연장이 걸려서 안전하긴 하다. 하지만 위에서 본 것처럼 어디까지 연장되는지가 미묘하고, 변수로 담는 것보다 싸지도 않다. 읽는 사람이 “이거 괜찮은 건가?” 하고 멈추게 만든다면 그 자체로 비용이다.
정리
- 임시는 전체 표현식이 끝나면 죽는다. 세미콜론이 대개 그 경계다.
- 임시 안을 가리키는 포인터·참조를 그 너머로 들고 나가면 안 된다.
c_str()이 대표적이지만 getter 전부가 같은 부류다. - 크래시가 아니라 오염이다. 값이 조용히 바뀐다.
-Wextra를 켜면-Wdangling-reference가 미묘한 케이스를 잡아준다. GCC 13부터다.c_str()댕글링은 아무 도구도 안 잡는다. 이건 알고 피하는 수밖에 없다.- 임시에 묶인 참조 반환은 C++26에서 컴파일 에러가 된다.
- 의심스러우면 그냥 변수에 담는다. 공짜다.
참고
- Abseil Tip of the Week #5: Disappearing Act — 임시가 전체 표현식 끝에서 소멸한다는 규칙,
.name(하위 객체)과.nickname()(멤버 함수 반환)의 수명 연장 차이를 참고했다. 2020-06-01 갱신본이라-Wdangling-reference(GCC 13)와 P2748R5(C++26)는 다루지 않는다. - P2748R5 — Disallow Binding a Returned Glvalue to a Temporary — 참조 반환 함수에서 임시에 묶는 return이 C++26에서 ill-formed가 된다는 내용을 확인했다.
- GCC Warning Options —
-Wdangling-reference가-Wextra에 포함되고 “references that refer to temporaries whose lifetime has ended”를 잡는다는 것을 확인했다.