← 문제로

53. 범위 기반 for 루프의 임시 객체 수명 함정 (dangling)

난이도 상
내 리뷰 · C++
해설 · C++

해설 — 범위 기반 for 루프의 임시 객체 수명 함정 (dangling)

난이도: 상

요약

범위 기반 for는 auto&& __range = <range-init>; 로 시작하도록 탈설탕된다. C++17/20에서 수명 연장(lifetime extension)은 __range가 직접 바인딩한 임시에만 적용된다. loopA는 __range가 MakePacket() 임시(Packet) 자체에 바인딩하므로 루프 동안 임시가 살아있어 안전하다. 반면 loopB는 __range가 payload()가 반환한 임시의 멤버에 대한 참조에 바인딩하는데, 이 참조는 임시를 연장하지 못한다 → MakePacket() 임시가 전체 for 문(초기화식 끝)에서 파괴되고, 루프 본문은 이미 파괴된 벡터를 순회한다(댕글링, UB).

핵심 개념

범위 기반 for for (T x : R) body 는 대략 다음과 같이 확장된다(C++11~20):

{
    auto&& __range = R;                 // (핵심) 여기서만 수명 연장이 적용됨
    auto __begin = begin(__range);
    auto __end   = end(__range);
    for (; __begin != __end; ++__begin) {
        T x = *__begin;
        body;
    }
}
  • R이 prvalue 임시면 __range(참조)가 그 임시에 바인딩되어 임시 수명이 __range 스코프(=for 문 전체)로 연장된다 → 안전.
  • R이 임시의 하위객체/멤버를 반환하는 함수 호출이면, __range는 그 반환 참조에 바인딩될 뿐 **바깥 임시(부모 객체)**를 연장하지 못한다. 전체 표현식 평가가 끝나면 부모 임시가 소멸 → __range는 댕글링.

단계별 내부 동작

  • loopA: __range = MakePacket().payload_ — payload_는 임시 Packet의 멤버지만, C++는 여기서 __range가 참조하는 **완전한 임시(Packet)**를 연장한다(멤버 접근을 통한 바인딩이라도 부모 임시가 대상). 루프 내내 유효.
  • loopB: __range = MakePacket().payload() — payload()는 const vector&를 반환한다. __range는 이 반환 참조에 바인딩되고, 반환 참조는 부모 임시의 수명을 연장하지 못한다. MakePacket() 임시는 초기화식 끝에서 파괴되고, 이후 begin/end·역참조는 해제된 메모리 접근.
  • 실측 증상: 최적화 수준·플랫폼에 따라 정상처럼 보이거나, 쓰레기 값·크래시. ASan을 켜면 heap-use-after-free로 잡힌다.

흔한 오해·함정

  • "범위 기반 for가 알아서 임시를 살려준다" — 함수가 참조를 반환하는 경우엔 아니다. getter 체인(obj().items(), map.at(k).list() 등)이 대표적 지뢰.
  • 표준 라이브러리에서도 for (auto c : std::string("x") + "y") 처럼 임시를 만드는 건 안전하지만, for (auto c : getConfig().name()) 처럼 참조 반환 getter를 임시에 대고 부르면 위험.

수정 방법 (지금 당장)

Packet pkt = MakePacket();                 // 이름 있는 객체로 수명 확보
for (int x : pkt.payload()) { /* 안전 */ }

또는 값으로 복사해 순회, 혹은 함수가 참조 대신 값을 반환하도록 설계.

C++23 완화

C++23은 범위 기반 for의 초기화식에서 생성된 임시들의 수명을 루프 전체로 연장하도록 규칙을 바꿔 loopB 같은 패턴의 대표적 댕글링을 없앴다(P2718R0). 다만 여전히 컴파일러/표준 모드 의존적이므로, 이식성 있는 코드는 임시를 이름 있는 변수로 받는 습관이 안전하다.

면접 포인트

  • 범위 기반 for의 desugaring과 auto&& __range의 역할.
  • 수명 연장이 "직접 바인딩된 임시"에만 적용되고 "참조 반환값"에는 적용되지 않는 이유.
  • C++23 P2718 변경과, 그 전까지의 안전한 관용구(임시를 named 변수로).