C/C++ 동작의 세 가지 분류
C와 C++ 표준은 프로그램의 동작을 미정의 동작(Undefined Behavior), 구현 정의 동작(Implementation-defined Behavior), 미지정 동작(Unspecified Behavior)이라는 세 가지 범주로 나누어 규정합니다. 이 세 가지는 비슷해 보이지만 컴파일러와 개발자에게 부여되는 책임과 보장 수준이 완전히 다릅니다.
1. 미정의 동작 (Undefined Behavior)
미정의 동작은 말 그대로 표준에서 어떠한 동작도 정의하지 않는 경우입니다. 언어 문법적으로는 유효한 코드임에도 불구하고, 실제 실행될 때 어떤 일이 일어날지 전혀 예측할 수 없습니다.
대표적인 예로 다음과 같은 표현식이 있습니다.
i = i++ + ++i;하나의 시퀀스 포인트 안에서 같은 변수 i를 여러 번 수정하기 때문에, 컴파일러마다 전혀 다른 결과가 나올 수 있습니다. 마찬가지로 u = (u++); 역시 수정 순서가 규정되어 있지 않아 미정의 동작에 해당합니다.
미정의 동작의 예는 이 외에도 많습니다. 배열 범위를 벗어난 접근, NULL 포인터 역참조, 부호 있는 정수의 오버플로 등이 모두 여기에 속합니다. 중요한 점은 컴파일러가 이런 코드에 대해 아무런 보장도 하지 않는다는 것입니다. 겉보기에는 정상 작동하는 것처럼 보일 수도 있고, 프로그램 전체가 비정상적으로 동작할 수도 있습니다.
2. 구현 정의 동작 (Implementation-defined Behavior)
구현 정의 동작은 표준이 구체적인 동작을 규정하지 않고, 컴파일러(구현체)가 스스로 선택하여 반드시 문서화하도록 요구하는 경우입니다.
여기서 핵심은 두 가지입니다.
- 구현체가 동작을 직접 결정한다.
- 그 결정을 문서로 명확히 밝혀야 하며, 해당 구현에서는 항상 그 동작만 발생하는 것이 보장된다.
예를 들어 int 자료형의 크기나 부호 있는 정수를 오른쪽 시프트할 때의 동작 방식은 플랫폼과 컴파일러에 따라 달라질 수 있지만, 각 구현체는 자신의 선택을 문서에 명시해야 하고 개발자는 그 문서를 참고해 예측 가능한 코드를 작성할 수 있습니다.
3. 미지정 동작 (Unspecified Behavior)
미지정 동작은 표준이 두 가지 이상의 가능한 동작을 허용하면서도, 어느 것이 선택될지 구현체에게 문서화나 보장을 요구하지 않는 경우입니다.
즉, 결과가 항상 유효한 값 중 하나로 나오는 것은 보장되지만, 매번 같은 결과가 나온다는 보장은 없습니다. 대표적인 예로 함수 호출 시 인자(argument)들의 평가 순서가 있습니다. f(g(), h())처럼 호출하면 g()와 h() 중 무엇이 먼저 실행될지 표준이 정하지 않으며, 컴파일러는 둘 중 편한 순서를 자유롭게 선택할 수 있습니다.
세 가지 동작의 핵심 차이 요약
| 분류 | 표준의 태도 | 문서화 의무 | 결과 예측 가능성 |
|---|---|---|---|
| 미정의 동작 | 아무것도 정의하지 않음 | 없음 | 전혀 예측 불가 |
| 구현 정의 동작 | 구현체가 선택 | 반드시 문서화 필요 | 문서 확인 후 예측 가능 |
| 미지정 동작 | 여러 동작 허용 | 없음 | 유효한 결과 중 하나가 나옴 |
결론적으로, 미정의 동작은 피해야 할 버그의 원인이고, 구현 정의 동작은 문서를 확인하면 안심하고 사용할 수 있는 동작이며, 미지정 동작은 결과가 유효하다는 점만 보장되는 동작입니다. 안전하고 이식성 높은 C/C++ 코드를 작성하려면 이 세 가지의 경계를 정확히 이해하는 것이 필수적입니다.