Namespaces
Variants

Undefined behavior

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

언어의 특정 규칙을 위반할 경우 전체 프로그램을 무의미하게 만듭니다.

목차

설명

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 이 구별되는지 여부, 배열 할당 오버헤드의 양 등.
  • 각 미지정 동작은 유효한 결과 집합 중 하나를 산출합니다.
  • erroneous behavior - 구현체가 진단하는 것을 권장하는 (잘못된) 동작
  • 잘못된 동작은 항상 올바르지 않은 프로그램 코드의 결과입니다.
  • 상수 표현식의 평가는 결코 잘못된 동작을 초래하지 않습니다.
  • 실행에 잘못된 동작을 갖는 것으로 명시된 연산이 포함된 경우, 구현체는 진단 메시지를 출력하는 것이 허용되며 권장되며, 해당 연산 이후 지정되지 않은 시점에 실행을 종료하는 것이 허용됩니다.
  • 구현체는 프로그램 동작에 대한 구현체별 가정 집합 하에서 잘못된 동작에 도달할 수 있다고 판단되면 진단 메시지를 출력할 수 있으며, 이는 오탐(false positive)을 초래할 수 있습니다.
잘못된 동작의 예시
#include <cassert>
#include <cstring>
void f()
{   
    int d1, d2;       // d1, d2 have erroneous values
    int e1 = d1;      // erroneous behavior
    int e2 = d1;      // erroneous behavior
    assert(e1 == e2); // holds
    assert(e1 == d1); // holds, erroneous behavior
    assert(e2 == d1); // holds, erroneous behavior
    std::memcpy(&d2, &d1, sizeof(int)); // no erroneous behavior, but
                                        // d2 has an erroneous value
    assert(e1 == d2); // holds, erroneous behavior
    assert(e2 == d2); // holds, erroneous behavior
}
unsigned char g(bool b)
{
    unsigned char c;     // c has erroneous value
    unsigned char d = c; // no erroneous behavior, but d has an erroneous value
    assert(c == d);      // holds, both integral promotions have erroneous behavior
    int e = d;           // erroneous behavior
    return b ? d : 0;    // erroneous behavior if b is true
}
(C++26부터)
  • undefined behavior - 프로그램의 동작에 대한 제한이 없습니다.
  • 런타임-정의되지 않은 동작 - 핵심 상수 표현식 의 평가 중에 발생하는 경우를 제외하고 정의되지 않은 동작.
(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에서 관찰된 것입니다.

#include <cstdio>
int main()
{
    bool p; // uninitialized local variable
    if (p)  // UB access to uninitialized scalar
        std::puts("p is true");
    if (!p) // UB access to uninitialized scalar
        std::puts("p is false");
}

가능한 출력:

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.

참고문헌

확장 콘텐츠
  • C++23 표준 (ISO/IEC 14882:2024):
  • 3.25 ill-formed program [defns.ill.formed]
  • 3.26 implementation-defined behavior [defns.impl.defined]
  • 3.66 unspecified behavior [defns.unspecified]
  • 3.68 well-formed program [defns.well.formed]
  • C++20 표준 (ISO/IEC 14882:2020):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++17 표준 (ISO/IEC 14882:2017):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++14 표준 (ISO/IEC 14882:2014):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++11 표준 (ISO/IEC 14882:2011):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]
  • C++98 표준 (ISO/IEC 14882:1998):
  • TBD ill-formed program [defns.ill.formed]
  • TBD implementation-defined behavior [defns.impl.defined]
  • TBD unspecified behavior [defns.unspecified]
  • TBD well-formed program [defns.well.formed]

참고 항목

[[ assume ( expression )]]
(C++23)
주어진 지점에서 표현식 이 항상 true 로 평가됨을 지정합니다
(속성 지정자)
(C++26)
초기화되지 않은 객체가 불확정 값을 가짐을 지정합니다
(속성 지정자)
도달할 수 없는 실행 지점을 표시합니다
(함수)
C 문서 에서 정의되지 않은 동작 참조

외부 링크

1. LLVM 프로젝트 블로그: 모든 C 프로그래머가 undefined behavior에 대해 알아야 할 것들 #1/3
2. LLVM 프로젝트 블로그: 모든 C 프로그래머가 undefined behavior에 대해 알아야 할 것들 #2/3
3. LLVM 프로젝트 블로그: 모든 C 프로그래머가 undefined behavior에 대해 알아야 할 것들 #3/3
4. Undefined behavior는 시간 여행을 초래할 수 있습니다 (다른 것들도 있지만 시간 여행이 가장 기이합니다)
5. C/C++에서의 정수 오버플로 이해하기
6. NULL 포인터와의 즐거움, 1부 (널 포인터 역참조로 인한 UB로 발생한 Linux 2.6.30의 로컬 익스플로잇)
7. Undefined Behavior와 페르마의 마지막 정리
8. C++ 프로그래머를 위한 undefined behavior 가이드