Undefined behavior
언어의 특정 규칙을 위반할 경우 전체 프로그램을 무의미하게 만듭니다.
목차 |
설명
C++ 표준은 다음 범주에 속하지 않는 모든 C++ 프로그램의 관찰 가능한 동작 을 정확하게 정의합니다:
- ill-formed - 프로그램에 구문 오류나 진단 가능한 의미론적 오류가 있습니다.
-
- 표준을 준수하는 C++ 컴파일러는 해당 코드에 의미를 부여하는 언어 확장(가변 길이 배열 등)을 정의하는 경우에도 진단 메시지를 반드시 출력해야 합니다.
- 표준 문서에서는 이러한 요구사항을 나타내기 위해 shall , shall not , 그리고 ill-formed 용어를 사용합니다.
- 형식이 잘못됨, 진단 불필요 - 프로그램이 일반적인 경우에 진단 가능하지 않을 수 있는 의미론적 오류를 가지고 있음 (예: ODR 위반 또는 링크 타임에만 감지 가능한 다른 오류들).
-
- 해당 프로그램이 실행되면 동작은 정의되지 않습니다.
- implementation-defined behavior - 프로그램의 동작이 구현체마다 다르며, 준수하는 구현체는 각 동작의 효과를 문서화해야 합니다.
-
- 예를 들어, std::size_t 의 타입이나 바이트의 비트 수, 또는 std::bad_alloc::what 의 텍스트 등이 있습니다.
- 구현 정의 동작의 하위 집합은 locale-specific behavior 로, 구현체가 제공하는 locale 에 따라 달라집니다.
- 미지정 동작 - 프로그램의 동작은 구현체마다 다르며, 준수하는 구현체가 각 동작의 효과를 문서화할 필요는 없습니다.
-
- 예를 들어, order of evaluation , 동일한 string literals 이 구별되는지 여부, 배열 할당 오버헤드의 양 등.
- 각 미지정 동작은 유효한 결과 집합 중 하나를 산출합니다.
|
(C++26부터) |
- undefined behavior - 프로그램의 동작에 대한 제한이 없습니다.
-
- 미정의 동작의 몇 가지 예시로는 데이터 레이스, 배열 범위 밖의 메모리 접근, 부호 있는 정수 오버플로우, 널 포인터 역참조, 하나의 표현식에서 동일한 스칼라에 대한 다중 수정 (중간 시퀀스 포인트 없이) (C++11 이전) (순서가 지정되지 않은) (C++11 이후) , 다른 타입의 포인터를 통한 객체 접근 등이 있습니다.
- 구현체가 미정의 동작을 진단할 필요는 없으며(단순한 많은 상황에서는 진단됨), 컴파일된 프로그램이 의미 있는 동작을 할 필요도 없습니다.
|
(C++11부터) |
UB와 최적화
올바른 C++ 프로그램은 정의되지 않은 동작을 포함하지 않기 때문에, 실제로 UB가 있는 프로그램이 최적화를 활성화한 상태로 컴파일될 때 컴파일러가 예상치 못한 결과를 생성할 수 있습니다:
예를 들어,
부호 있는 오버플로
int foo(int x) { return x + 1 > x; // 참이거나 부호 있는 오버플로우로 인한 UB }
다음과 같이 컴파일될 수 있음 ( demo )
foo(int): mov eax, 1 ret
범위를 벗어난 접근
int table[4] = {}; bool exists_in_table(int v) { // 처음 4번의 반복 중 하나에서 true를 반환하거나, 범위를 벗어난 접근으로 인해 미정의 동작 발생 for (int i = 0; i <= 4; i++) if (table[i] == v) return true; return false; }
다음과 같이 컴파일될 수 있음 ( demo )
exists_in_table(int): mov eax, 1 ret
초기화되지 않은 스칼라
std::size_t f(int x) { std::size_t a; if (x) // x가 0이 아니거나 UB인 경우 a = 42; return a; }
다음과 같이 컴파일될 수 있음 ( demo )
f(int): mov eax, 42 ret
표시된 출력은 이전 버전의 gcc에서 관찰된 것입니다.
가능한 출력:
p is true p is false
잘못된 스칼라
int f() { bool b = true; unsigned char* p = reinterpret_cast<unsigned char*>(&b); *p = 10; // b에서 읽는 것은 이제 UB입니다 return b == 0; }
다음과 같이 컴파일될 수 있음 ( 데모 )
f(): mov eax, 11 ret
널 포인터 역참조
이 예제들은 널 포인터를 역참조한 결과를 읽는 것을 보여줍니다.
int foo(int* p) { int x = *p; if (!p) return x; // 위의 코드가 UB이거나 이 분기는 절대 실행되지 않음 else return 0; } int bar() { int* p = nullptr; return *p; // 무조건적인 UB }
다음과 같이 컴파일될 수 있습니다 ( 데모 )
foo(int*): xor eax, eax ret bar(): ret
std::realloc에 전달된 포인터 접근 std::realloc
출력을 확인하려면 clang을 선택하십시오
#include <cstdlib> #include <iostream> int main() { int* p = (int*)std::malloc(sizeof(int)); int* q = (int*)std::realloc(p, sizeof(int)); *p = 1; // UB access to a pointer that was passed to realloc *q = 2; if (p == q) // UB access to a pointer that was passed to realloc std::cout << *p << *q << '\n'; }
가능한 출력:
12
부작용 없는 무한 루프
출력을 확인하려면 clang 또는 최신 gcc를 선택하십시오.
#include <iostream> bool fermat() { const int max_value = 1000; // Non-trivial infinite loop with no side effects is UB for (int a = 1, b = 1, c = 1; true; ) { if (((a * a * a) == ((b * b * b) + (c * c * c)))) return true; // disproved :() a++; if (a > max_value) { a = 1; b++; } if (b > max_value) { b = 1; c++; } if (c > max_value) c = 1; } return false; // not disproved } int main() { std::cout << "Fermat's Last Theorem "; fermat() ? std::cout << "has been disproved!\n" : std::cout << "has not been disproved.\n"; }
가능한 출력:
Fermat's Last Theorem has been disproved!
진단 메시지와 함께 형식 오류
컴파일러가 잘못된 형식의 프로그램에 의미를 부여하는 방식으로 언어를 확장할 수 있음에 유의하십시오. C++ 표준이 이러한 경우에 요구하는 유일한 사항은 진단 메시지(컴파일러 경고)이며, "진단이 필요하지 않은 잘못된 형식" 프로그램인 경우는 예외입니다.
예를 들어, 언어 확장이
--pedantic-errors
를 통해 비활성화되지 않는 한, GCC는 다음 예제를
경고만으로 컴파일합니다
비록 이것이 C++ 표준에서
"오류"의 예시로 등장함에도
불구하고 (참고:
GCC Bugzilla #55783
)
#include <iostream> // 예시 조정, 상수를 사용하지 마십시오 double a{1.0}; // C++23 표준, §9.4.5 목록 초기화 [dcl.init.list], 예시 #6: struct S { // initializer-list 생성자 없음 S(int, double, double); // #1 S(); // #2 // ... }; S s1 = {1, 2, 3.0}; // OK, #1 호출 S s2{a, 2, 3}; // 오류: 축소 변환 S s3{}; // OK, #2 호출 // — 예시 끝] S::S(int, double, double) {} S::S() {} int main() { std::cout << "All checks have passed.\n"; }
가능한 출력:
main.cpp:17:6: error: type 'double' cannot be narrowed to 'int' in initializer ⮠
list [-Wc++11-narrowing]
S s2{a, 2, 3}; // error: narrowing
^
main.cpp:17:6: note: insert an explicit cast to silence this issue
S s2{a, 2, 3}; // error: narrowing
^
static_cast<int>( )
1 error generated.
참고문헌
| 확장 콘텐츠 |
|---|
|
참고 항목
[[
assume
(
expression
)]]
(C++23)
|
주어진 지점에서
표현식
이 항상
true
로 평가됨을 지정합니다
(속성 지정자) |
[[
indeterminate
]]
(C++26)
|
초기화되지 않은 객체가 불확정 값을 가짐을 지정합니다
(속성 지정자) |
|
(C++23)
|
도달할 수 없는 실행 지점을 표시합니다
(함수) |
|
C 문서
에서
정의되지 않은 동작
참조
|
|