TECH 으로 돌아가기
TECH HACKER NEWS 오늘 7분 읽기 28 READS

C++26, 'while(true);' 무한 루프의 미정의 동작을 끝내다

다음 프로그램은 잘 정의되어 있을까. int main() { while (true) ; } 직관적으로는 '그냥 영원히 도는 빈 루프'로 보이지만, C++26 이전 표준에서는 이 코드가 미정의 동작(undefined behaviour, UB)이었다. 부수 효과가 전혀 없는 무한 루프는 언젠가 종료된다고 컴파일러가 가정할 수 있었고, 그 가정 위에서 최적화기는 루프 자체를 통째로 제거할 수 있었다. 특히 Clang이 이 최적화를 적극적으로 수행했다.

그 결과는 상식을 벗어난다. 원문이 든 예시에서는 main에 빈 무한 루프만 있고 그 아래에 "Hello world!"를 출력하는 별도 함수 unreachable()이 정의돼 있는데, Clang으로 컴파일하면 실제로 "Hello world!"가 출력된다. 컴파일러가 무한 루프를 지워버리자 main이 아래로 흘러 내려가고, 링커가 뒤에 배치한 함수의 코드가 그대로 실행되기 때문이다. 이는 컴파일러 버그가 아니라 UB에 대한 합법적인 해석이며, 표준이 허용한 자유를 그대로 행사한 것이다.

왜 UB였나: 전진 보장 규칙

문제의 뿌리는 C++11이 스레딩과 함께 도입한 '전진 보장(forward progress guarantee)'에 있다. 표준(intro.progress)은 구현이 모든 스레드가 결국 다음 중 하나를 수행한다고 가정할 수 있다고 규정한다. 즉 종료하거나, 라이브러리 입출력 함수를 호출하거나, volatile glvalue에 접근하거나, 동기화 혹은 원자적 연산을 수행한다는 것이다. while (true); 루프는 이 넷 중 어느 것도 하지 않는다. 따라서 이 루프에 영원히 머무는 실행은 UB로 간주됐고, 최적화기는 '실행이 여기 갇힐 리 없다'고 전제한 뒤 루프를 없애고 그 경로를 도달 불가능(unreachable)으로 표시할 수 있었다.

흥미로운 대목은 C 언어가 이 문제를 처음부터 바로잡아 두었다는 점이다. C11도 C++11과 같은 시기에 전진 보장 규칙을 들여왔지만, C는 규칙 하나를 더 두었다. 제어 표현식이 상수 표현식인 루프는 종료한다고 가정하지 않는다는 조항이다. 덕분에 while (1);은 C11 이후로 줄곧 잘 정의된 코드였다. C++는 이 추가 규칙을 채택하지 않았고, 그 결과 같은 문법이 C에서는 정상, C++에서는 UB라는 불필요한 괴리가 오래 남았다.

실무에서 왜 위험한가

누가 굳이 while (true);를 쓰느냐고 물을 수 있지만, 이 패턴은 임베디드와 커널 코드에서 흔한 '오류 시 정지(halt-on-error)' 관용구다. 치명적 오류가 발생했는데 빠져나갈 운영체제가 없는 환경에서는 그냥 그 자리에 멈춰 서는 것이 안전한 선택이다. 그런데 최적화기가 이 정지 루프를 제거하면, 실행은 링커가 뒤에 배치한 임의의 코드로 흘러 들어간다. 임베디드 시스템이라면 치명적 오류 처리기가 실제로는 장치를 멈추지 못하고, 하드웨어가 손상된 상태로 계속 명령을 실행하게 된다. 보안이 중요한 코드에서는 이것이 곧 실제 취약점이 된다. 즉 UB는 이론적 우려가 아니라 동작하는 코드를 조용히 망가뜨리는 실질적 위험이었다.

C++26의 해법: '사소한 무한 루프'

C++26은 P2809R3을 통해 이 문제를 바로잡되, C의 규칙을 그대로 복사하지는 않았다. C 방식은 상수 제어 표현식을 가진 넓은 범위의 루프를 보호하기 때문에 유용한 최적화까지 억제할 수 있다는 이유로 검토 끝에 기각됐다. 대신 표준은 '사소한 무한 루프(trivial infinite loop)'라는 의도적으로 좁은 범주를 새로 정의했다. 두 조건을 모두 만족해야 한다. 첫째, 루프 본문이 문자 그대로 비어 있어야 한다(; 또는 {}). "a string"; 같은 무의미한 표현식 문장이라도 본문에 들어가면 자격을 잃는다. 둘째, 제어 표현식이 상수 표현식이면서 참으로 평가돼야 한다. 조건이 없는 for 루프의 경우 참이 암묵적으로 적용된다.

두 조건이 충족되면 루프 본문은 std::this_thread::yield() 호출로 대체되며, 이로써 그동안 없던 전진 보장 의미론을 얻는다. 전진 보장 규칙 자체도 갱신돼, 스레드가 '사소한 무한 루프의 실행을 계속하는 것'이 결국 수행할 수 있는 동작 중 하나로 추가됐다. 따라서 최적화기는 더 이상 이런 루프를 UB로 취급해 뒤로 흘러가는 실행을 가정할 수 없다. 또 이 제안은 결함 보고(defect report)로도 채택돼, 구현체가 이전 C++ 모드에까지 수정을 적용할 수 있다. 최신 컴파일러라면 C++20 모드에서도 예전의 위험한 동작이 재현되지 않을 수 있는 이유가 여기에 있다.

한 가지 한계는 짚어둘 만하다. 프리스탠딩(freestanding) 구현에서는 std::this_thread::yield()로의 대체가 실제로 일어나는지 여부가 구현 정의 사항으로 남는다. 이는 베어메탈 시스템에 중요하다. 의도적인 정지 루프를 협력적 양보(yield)로 바꾸면 프로그래머가 전혀 의도하지 않은 동작이 끼어들 수 있기 때문이다. 결국 이번 변경은 안전성을 강화하는 방향의 최근 C++ 흐름과 궤를 같이하되, 정지가 곧 안전인 환경에서는 컴파일러 구현과 대상 플랫폼의 동작을 여전히 확인해 두어야 한다는 뜻이다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://www.sandordargo.com/blog/2026/09/16/cpp26-trivial-in...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...