정의되지 않은 동작(UB) 을 구현마다 다른 동작을 허용한다는 뜻으로 받아들이면 안 됨. 그런 목적을 위한 구현 정의 동작(implementation-defined behavior) 이라는 용어가 이미 따로 존재함.
UB는 컴파일러에 아무런 제약도 부과하지 않음. 표준이 어떤 조건에서의 결과를 명시적으로 미정의로 두거나 아예 규정하지 않았다면, 컴파일러는 무엇이든 하거나 아무것도 하지 않거나 위치마다 다르게 처리할 수 있음. 구현체·플랫폼·하드웨어가 어떻게 동작하는지는 언어 차원의 UB 여부를 바꾸지 못함.
이런 글과 설명을 보면 C와 C++의 UB에 대한 부정적인 인식이 커질 수밖에 없음. 설명이 틀렸다거나 누군가를 탓하려는 게 아니라, 제시된 예제 자체가 정말 섬뜩하다는 뜻임.
반환 없이 함수 끝에 도달하는 예제는 간단하면서도 충격적이라 자주 등장함. Godbolt example the author provides에 적용해 보면 -fsanitize=unreachable로 대응할 수 있는 듯함. other, similar examples를 잡는 다른 옵션도 있지만, 글의 결론을 실질적으로 바꾸는지는 모르겠음.
그렇다고 컴파일러 개발자가 제정신이 아니거나 악의적이라는 뜻도 아님. previous discussions의 ~david_chisnall과 ~lonjil은 UB가 있는 코드에서도 합리적으로 동작하는 최적화 컴파일러를 만드는 것은 불가능하며, 실질적인 해결책은 다른 언어들처럼 UB를 만들기 어렵거나 불가능하게 하는 것이라고 설명함. Chris Lattner도 “advantages of undefined behaviour in C”를 명시적으로 다룬 바 있음.
한편 ~matklad는 모든 UB가 트랩을 발생시키도록 정의한 가상의 Lil-C를 두고 유용한 일을 한 게 아니라고 하지만, 내게는 오히려 꽤 유용해 보임.
UB의 결과를 제한할 수 없다면 잘못된 변환을 사후에 탐지하는 데도 한계가 있음. 여러 합리적인 최적화가 맞물려 결과가 나타나는 만큼, lists of undefined behaviours in C를 읽고 주의하는 것만으로 피할 수 있으리라고 기대하기도 어려워 답답함.
단순하고 빠르고 안전하며 UB도 없고 최적화도 포기하지 않는 언어를 바라는 마음은 있음. 새 코드에서는 C를 떠난다기보다 Zig 쪽으로 끌리고 있지만, 기존 C·C++ 코드를 재작성하지 않고 계속 크게 수정하고 싶기도 함. 그 선택의 위험을 안다고 해서 손 놓고 있을 수는 없음. 앞으로 작성할 C 코드 10만 줄에서 실제로 무엇을 해야 할까? 그저 허우적대다 가라앉는 것 외에 할 수 있는 일이 없을까?
구현체는 C 표준이 명시적·암묵적으로 정의하지 않은 동작을 별도로 정의할 수 있음. POSIX처럼 표준 C가 정의하지 않은 일부 동작을 규정하는 다른 표준도 존재함.
모든 UB가 그렇다거나 UB를 만드는 모든 C 개발자가 이를 이해한다는 뜻은 아님. 다만 특정 표준이나 구현체를 대상으로 코드를 작성하면서, 그쪽이 명시적으로 보장한 동작에 의식적으로 의존하는 것은 잘못이 아님.
그래도 C를 써야 할지 조언을 구하는 단계라면 C를 쓰지 않는 편이 낫다고 봄. 이미 C를 쓰며 계속 써도 될지 묻지 않는 개발자라도, 가능하다면 실수하기 쉬운 함정이 적은 언어로 옮기는 것을 진지하게 고려해야 함.
내 요점은 UB라면 구현체가 무엇이든 할 수 있으며, 개발자가 그 동작을 보장받는다는 뜻은 아니라는 것임. 그런데 왜 내가 UB에 의존하려는 행위 자체를 비난한 것처럼 답하는지 이해하기 어려움.
내가 의존을 금지하자고 한 게 아니라, C와 C++이 해당 연산을 UB로 분류함으로써 언어 차원의 보장을 하지 않는다는 뜻임. 원하는 보장을 얻으려면 위원회를 설득해 해당 동작을 구현 정의·미지정·오류 동작으로 규정하면 됨.
명세의 명확한 문구와 실제 컴파일러 동작을 부정하는 사람들과 더 논쟁하기보다는, 표준을 개선하는 데 집중하겠음.
사람들이 그렇게 답하는 건, 바로 당신이 제안한 대로 하고 있기 때문임. 앞서 Linux 커널처럼 표준 C의 한계를 인정하라고 했는데, 실제로 많은 C 개발자는 표준 C가 아니라 POSIX C, GNU C, 또는 GNU C와 MSVC C의 공통 영역을 대상으로 코드를 작성함.
무엇인가를 비판하고 있고 그 대상이 다른 사람들의 작업과 관련 있다는 것 외에는, 정확히 무엇을 문제 삼는지 분명하지 않음. 주변 사람들이 실제로 날고 있는데 연단에 올라 “당신들은 절대 날 수 없다”고 외치는 셈임. 가끔 추락하거나 우연히 성공하는 것이라 해도 날고 있는 것은 사실임. 그 말이 현실과 어떻게 연결되는지 설명하지 않으면 도움이 되지 않음.
도달 가능한 위치에 std::unreachable() 을 쓰거나 명백한 널 포인터를 역참조하면 컴파일러가 황당한 코드를 만들 수는 있음. 하지만 개발자가 보통 그런 코드를 쓰지는 않으며, 컴파일러 개발자들도 이런 결과를 줄이려 노력 중임. 본문에 나온 GCC 출력도 그 예임. 이를 UB가 언제나 예측 불가능한 결과를 만든다는 식으로 제시하는 것은 부적절해 보임. std::unreachable()은 안전한 최적화를 돕기 위한 장치이기도 함. 컴파일러는 확실한 UB가 보이는 경로에 ud2 같은 트랩 명령을 넣되, 개발자가 도달 불가라고 표시한 경로에서는 생략할 수 있음. 단순히 UB의 존재만으로 실행되지 않을 경로를 추론하는 것 외에도 대안이 있는 셈임. 따라서 이 기능을 잘못 쓰면 다른 UB에서는 있을 수도 있는 안전장치마저 사라지므로 특히 조심해야 함. 모든 UB를 한데 묶는 것은 오해를 부름. 널 포인터 역참조는 보통 즉시 충돌하지만, 버퍼 범위 초과나 해제 후 사용은 훨씬 은밀하고 위험할 수 있음. 반대로 소스 파일 끝의 줄바꿈 누락도 UB라는 예를 들면서, 실제로는 경고 외에 유의미한 차이를 만들지 않는다는 사실은 빼놓곤 함.
유효하지 않은 포인터의 역참조를 제외하면, 실제 실행 중 발생하는 UB 대다수는 무해하거나 예상한 대로 동작할 것이라고 강하게 추측함. 정수 오버플로가 하드웨어 동작대로 순환하는 경우가 그 예임. 물론 컴파일러가 오버플로 검사를 없애거나 초기화되지 않은 변수의 로그 출력을 없애는 유명한 예외도 있음. 후자는 진행 중인 언어 변경으로 앞으로 문제가 해소될 것으로 봄. 이번 글도 이전 글처럼 불필요하게 선정적이라고 느낌.