본문 바로가기
개발2026년 9월 18일7분 읽기

C++26, while(true); 가 드디어 정의된 동작이 된다 — 임베디드 개발자가 기다리던 수정

YS
김영삼
조회 228
C++26, while(true); 가 드디어 정의된 동작이 된다 — 임베디드 개발자가 기다리던 수정

C++26이 P2809R3 제안을 채택해, 자명한 무한 루프(trivial infinite loop)를 미정의 동작(UB)에서 제외한다. 대상은 본문이 비어 있고(; 또는 {}) 제어 조건이 상수 표현식으로 true인 루프다. for(;;)처럼 조건이 생략돼 암묵적으로 true인 경우도 포함된다.

기존 표준에서 이런 루프는 부작용이 없다는 이유로 UB였고, 컴파일러가 통째로 제거해 버릴 수 있었다. 그 결과 오류 처리 루틴이 무한 대기하는 대신 다음 코드로 흘러 들어가는 일이 실제로 일어났다.

이 문제를 처음 겪은 사람은 대부분 같은 반응을 보인다. "컴파일러가 내 무한 루프를 지웠다고?" 농담처럼 들리지만 사실이다. 표준이 전진 보장(forward progress)을 요구하기 때문에, 관측 가능한 부작용이 없는 무한 루프는 도달할 수 없는 코드로 간주돼 최적화 과정에서 사라질 수 있었다.

무엇이 문제였나

// 임베디드에서 흔한 치명적 오류 처리 패턴
void panic() {
    disable_interrupts();
    log_fault();
    while (true) { }      // 여기서 멈춰야 한다
}

// C++26 이전: 이 루프는 부작용이 없으므로 UB.
// 일부 컴파일러는 루프를 제거하고, 실행은 panic() 다음으로 "떨어진다".
// 결과: 고장난 상태의 하드웨어가 계속 임의의 코드를 실행한다.
보안까지 걸린 문제 오류 처리에서 멈추지 않고 실행이 계속되면, 손상된 상태로 다음 명령을 수행하게 된다. 커널이나 부트로더처럼 신뢰 경계를 지키는 코드에서 이것은 단순한 버그가 아니라 취약점이다. 방어 코드가 방어를 하지 않는 상황이기 때문이다.

C++26이 바꾸는 것

두 조건이 모두 성립하면 — 본문이 문법적으로 비어 있고, 제어 표현식이 상수 true로 평가되면 — 컴파일러는 루프 본문을 std::this_thread::yield() 호출로 대체한다. 이로써 UB가 아니라 전진 보장 의미를 갖는 정상적인 구성이 된다.

상황C++26 이전C++26
while (true) { }UB — 제거될 수 있음정의됨 — yield 의미로 유지
for (;;) { }UB — 제거될 수 있음정의됨 (암묵적 true 포함)
while (true) { x++; }부작용 있음 — 유지동일 (변화 없음)
while (cond) { } (비상수)문맥에 따라 다름대상 아님

프리스탠딩(freestanding) 구현에서는 yield 대체가 일어나는지 여부가 구현 정의로 남는다. 운영체제 스레드 개념이 없는 환경에서 yield를 요구하는 게 무리이기 때문이다. 베어메탈 타깃이라면 툴체인 문서를 확인해야 한다.

지금 코드에서 해야 할 일

1
정지 패턴 점검
오류 처리나 부트 실패 경로에서 빈 무한 루프를 쓰고 있는지 찾는다.
2
당장은 명시적 부작용 추가
표준이 적용되기 전까지는 루프 안에 실제 부작용을 넣어 제거를 막는 게 안전하다. 인라인 어셈블리 nop, 휘발성 변수 읽기, 아키텍처별 wfi/hlt 명령 등.
3
컴파일러 지원 확인
표준 채택과 컴파일러 구현은 별개다. 사용하는 컴파일러 버전이 P2809를 구현했는지 확인한다.
4
프리스탠딩 여부 확인
베어메탈·커널 타깃이면 yield 대체가 적용되지 않을 수 있으므로 명시적 정지 명령을 쓰는 편이 안전하다.
// 이식성 있는 안전한 정지 (표준 적용 전에도 통하는 형태)
[[noreturn]] void halt() {
    disable_interrupts();
    for (;;) {
#if defined(__arm__) || defined(__aarch64__)
        __asm__ volatile("wfi");     // 인터럽트 대기 (저전력 정지)
#elif defined(__x86_64__) || defined(__i386__)
        __asm__ volatile("hlt");
#else
        __asm__ volatile("" ::: "memory");  // 최소한 최적화 장벽
#endif
    }
}

더 큰 맥락 — UB를 줄여 가는 흐름

C++ 표준이 최근 몇 사이클 동안 해 온 일 중 눈에 띄는 것이 UB 축소다. 초기화되지 않은 변수 읽기에 대한 처리, 산술 관련 규정 정리, 그리고 이번 무한 루프까지. 배경에는 명확한 압력이 있다. 메모리 안전 언어와의 경쟁, 그리고 보안 규제 기관의 요구다.

UB는 원래 컴파일러에게 최적화 자유를 주기 위한 장치였다. 하지만 수십 년이 지나면서 그 자유가 만든 비용이 이득을 넘어서는 영역이 드러났다. "이 코드는 UB이므로 무슨 일이든 일어날 수 있다"는 설명은, 방어 코드를 쓰는 개발자에게 받아들이기 어려운 답이다. 이번 변경은 그 인식이 표준에 반영된 작은 사례다.

출처

자주 묻는 질문

왜 while(true); 가 미정의 동작이었나요?

C++ 표준이 전진 보장을 요구했기 때문입니다. 관측 가능한 부작용이 없는 무한 루프는 프로그램이 진행하지 않는 상태로 간주돼, 컴파일러가 도달 불가능한 코드로 판단하고 제거할 수 있었습니다.

C++26에서는 무엇이 달라지나요?

본문이 비어 있고 제어 조건이 상수 true인 루프에 한해 UB가 아니게 됩니다. 컴파일러는 루프 본문을 std::this_thread::yield() 호출로 대체해 전진 보장 의미를 부여합니다. 프리스탠딩 구현에서는 이 대체 여부가 구현 정의입니다.

기존 코드를 고쳐야 하나요?

표준 적용과 컴파일러 구현 시점이 다르므로, 당분간은 정지 루프 안에 실제 부작용을 넣는 편이 안전합니다. ARM의 wfi, x86의 hlt 같은 아키텍처 명령이나 최소한의 최적화 장벽을 사용하세요.

일반 애플리케이션 개발자에게도 영향이 있나요?

직접적인 영향은 적습니다. 주로 임베디드·커널·부트로더처럼 오류 시 정지 패턴을 쓰는 코드가 대상입니다. 다만 서버 코드에서도 바쁜 대기 루프를 쓴다면 의도치 않은 최적화 가능성을 점검해 볼 만합니다.

컴파일러가 이미 지원하나요?

표준 채택과 구현은 별개이며 컴파일러·버전마다 다릅니다. GCC 등에서 관련 구현 작업이 진행된 기록이 있으므로, 사용하는 툴체인의 릴리스 노트에서 P2809 지원 여부를 확인하는 것이 정확합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록