수색…


소개

정의되지 않은 동작 (UB)이란 무엇입니까? ISO C ++ 표준 (§1.3.24, N4296)에 따르면이 표준이 요구 사항을 부과하지 않는 동작입니다.

즉, 프로그램에서 UB를 만날 때 원하는대로 할 수 있습니다. 이것은 종종 충돌을 의미하지만, 단순히 아무것도하지 않거나, 귀신이 코 에서 빠지게 하거나, 제대로 작동하는 것처럼 보일 수도 있습니다!

말할 필요도없이 UB를 호출하는 코드를 작성하지 않아야합니다.

비고

프로그램에 정의되지 않은 동작이 포함되어 있으면 C ++ 표준은 동작에 제약을 가하지 않습니다.

  • 개발자가 의도 한대로 작동하는 것처럼 보일 수 있지만 충돌하거나 이상한 결과가 발생할 수 있습니다.
  • 동작은 동일한 프로그램 실행마다 다를 수 있습니다.
  • 정의되지 않은 동작을 포함하는 행 앞에 오는 행을 포함하여 프로그램의 모든 부분이 오작동 할 수 있습니다.
  • 구현시 정의되지 않은 동작의 결과를 문서화 할 필요는 없습니다.

구현 표준에 따라 정의되지 않은 동작을 생성하는 연산 결과를 문서화 할 수 있지만 문서화 된 동작에 의존하는 프로그램은 이식 할 수 없습니다.

정의되지 않은 동작이있는 이유

직관적으로 정의되지 않은 동작은 예외 처리기를 통해 그러한 오류를 우아하게 처리 할 수 ​​없으므로 나쁜 것으로 간주됩니다.

그러나 일부 동작을 정의되지 않은 상태로 두는 것은 실제로 C ++의 "당신이 사용하지 않는 것에 대해 비용을 지불하지 않는다"는 약속의 핵심 부분입니다. 정의되지 않은 동작은 컴파일러가 개발자가 자신이하는 일을 알고 있다고 가정하고 위의 예제에서 강조 표시된 실수를 확인하기위한 코드를 도입하지 못하게합니다.

정의되지 않은 동작 찾기 및 방지

개발 중에 정의되지 않은 동작을 발견하기 위해 일부 도구를 사용할 수 있습니다.

  • 대부분의 컴파일러에는 컴파일 타임에 정의되지 않은 동작에 대해 경고하는 경고 플래그가 있습니다.
  • gcc 및 clang의 최신 버전에는 런타임시 성능 비용으로 정의되지 않은 동작을 검사하는 소위 "정의되지 않은 동작 삭제 기"플래그 ( -fsanitize=undefined )가 포함되어 있습니다.
  • lint 같은 도구는 정의되지 않은 행동 분석을 철저히 수행 할 수 있습니다.

정의되지 않고 구체화 되지 않은 구현 정의 동작

C ++ 14 표준 (ISO / IEC 14882 : 2014) 섹션 1.9 (프로그램 실행) :

  1. 이 표준의 의미 론적 설명은 매개 변수화 된 비 결정적 추상 기계를 정의한다. [절단]

  2. 추상 기계의 특정 측면 및 오퍼레이션은이 표준에서 구현 정의 (예 : sizeof(int) )로 설명됩니다. 이것들 은 추상 기계의 매개 변수를 구성 합니다 . 각 구현에는 이러한 측면에서 특성과 동작을 설명하는 문서가 포함되어야합니다. [절단]

  3. 추상 기계의 특정한 다른 측면과 연산은이 규격에서 상세하지 않은 것으로 기술 되어있다 (예를 들어, 할당 기능이 메모리 할당에 실패 할 경우 new-initializer 의 표현식 평가). 가능한 경우,이 국제 표준은 허용 가능한 일련의 행동을 정의한다. 이것들은 추상 기계의 비 결정적 측면을 정의합니다. 따라서 추상 기계의 인스턴스는 주어진 프로그램과 주어진 입력에 대해 둘 이상의 가능한 실행을 가질 수 있습니다.

  4. 특정 다른 연산은이 표준에서 정의되지 않은 것으로 (또는 예를 들어, const 객체 수정 시도의 효과) 기술된다. [ :이 국제 표준은 정의되지 않은 동작을 포함하는 프로그램의 동작에 대한 요구 사항을 부과하지 않는다. - 끝말 ]

널 포인터를 통한 읽기 또는 쓰기

int *ptr = nullptr;
*ptr = 1; // Undefined behavior

null 포인터가 유효한 객체를 가리 키지 않으므로 *ptr 에 쓸 객체가 없기 때문에 이는 정의되지 않은 동작 입니다.

이것이 가장 흔히 세분화 오류를 일으키지 만, 정의되지 않았으며 아무 일도 발생할 수 없습니다.

void가 아닌 반환 유형이있는 함수에 대한 return 문이 없습니다.

반환 형식이 void 가 아닌 함수에서 return 문을 생략하면 정의되지 않은 동작 입니다.

int function() {  
    // Missing return statement
} 

int main() {
    function(); //Undefined Behavior
}

대부분의 현대 컴파일러는 이러한 종류의 정의되지 않은 동작에 대해 컴파일 타임에 경고를 내 보냅니다.


주 : main 규칙은 유일한 예외입니다. mainreturn 문이 없으면 컴파일러에서 자동으로 return 0; 삽입합니다 return 0; 당신을 위해, 그래서 그것은 안전하게 빠져 나갈 수 있습니다.

문자열 리터럴 수정하기

C ++ 11
char *str = "hello world";
str[0] = 'H';

"hello world" 는 문자열 리터럴이므로 수정하면 정의되지 않은 동작이 발생합니다.

위의 예제에서 str 의 초기화는 C ++ 03에서 공식적으로 사용되지 않으므로 (표준의 향후 버전에서 제거 예정) 2003 년 이전의 여러 컴파일러가 이에 대해 경고 할 수 있습니다 (예 : 의심스러운 변환). 2003 년 이후 컴파일러는 일반적으로 사용되지 않는 변환에 대해 경고합니다.

C ++ 11

위의 예제는 올바르지 않으며 C ++ 11 이상에서는 컴파일러 진단이됩니다. 다음과 같이 형식 변환을 명시 적으로 허용하여 정의되지 않은 동작을 나타내도록 유사한 예제를 구성 할 수 있습니다.

char *str = const_cast<char *>("hello world");
str[0] = 'H'; 

범위를 벗어난 인덱스에 액세스하기

배열 (또는 해당 배열에 대한 표준 라이브러리 컨테이너, 모두 원시 배열을 사용하여 구현되므로)에 대한 범위를 벗어나는 인덱스에 액세스하는 것은 정의되지 않은 동작입니다 .

 int array[] = {1, 2, 3, 4, 5};
 array[5] = 0;  // Undefined behavior

그것은 (이 경우에 배열의 끝 부분에 대한 포인터를 가리키는 가질 array + 5 ), 당신은 그냥 할 수없는 그것을 역 참조, 유효한 요소 아니므로.

 const int *end = array + 5;  // Pointer to one past the last index
 for (int *p = array; p != end; ++p)
   // Do something with `p`

일반적으로 범위를 벗어난 포인터는 작성할 수 없습니다. 포인터는 배열 내의 요소를 가리켜 야합니다.

0으로 정수 나누기

int x = 5 / 0;    // Undefined behavior

0 으로 나눈 0 은 수학적으로 정의되지 않았으므로 이것이 정의되지 않은 동작이라는 것을 이해할 수 있습니다.

하나:

float x = 5.0f / 0.0f;   // x is +infinity

대부분의 구현은 NaN (분자가 0.0f 인 경우), infinity (분자가 양수인 경우) 또는 -infinity (분자가 음수 인 경우)를 반환하기 위해 0으로 부동 소수점 나누기를 정의하는 IEEE-754를 구현합니다.

부호있는 정수 오버플로

int x = INT_MAX + 1;

// x can be anything -> Undefined behavior

표현식을 평가하는 동안 결과가 수학적으로 정의되지 않았거나 해당 유형의 표현 가능한 값 범위에 있지 않으면 동작은 정의되지 않습니다.

(C ++ 11 표준 단락 5/4)

이것은 일반적으로 재현 가능한 비 충돌 동작을 생성하므로 개발자가 관찰 된 동작에 크게 의존하도록 유혹받을 수 있으므로 더 불쾌한 것 중 하나입니다.


반면에 :

unsigned int x = UINT_MAX + 1;

// x is 0

잘 정의 된 이유는 다음과 같습니다.

부호없는 것으로 선언 된 부호없는 정수는 2^n 의 법칙을 따른다. 여기서 n 은 정수의 특정 값의 값 표현에있는 비트 수이다.

(C ++ 11 표준 단락 3.9.1 / 4)

때로는 컴파일러가 정의되지 않은 동작을 악용하여

signed int x ;
if(x > x + 1)
{
    //do something
}

여기에서 부호있는 정수 오버 플로우가 정의되지 않았기 때문에 컴파일러는 결코 발생할 수 없다고 가정 할 수 있으므로 "if"블록을 최적화 할 수 있습니다

초기화되지 않은 지역 변수 사용

int a;
std::cout << a; // Undefined behavior!

때문에 이것은 정의되지 않은 동작 결과 a 초기화되지 않은 것입니다.

값이 "불확정"이거나 "이전에 해당 메모리 위치에 있던 값이 무엇이든"이기 때문에 이것이 종종 잘못된 것이라고 주장합니다. 그러나 위의 예에서 정의되지 않은 동작을 제공하는 것은 a 의 값에 액세스하는 행위입니다. 실제로 "쓰레기 값"을 인쇄하는 것은이 경우 일반적인 증상이지만 정의되지 않은 동작의 한 가지 가능한 형태 일뿐입니다.

실제로 (특정 하드웨어 지원에 의존하기 때문에) 실제로는있을 법하지 않지만 컴파일러는 위의 코드 샘플을 컴파일 할 때 프로그래머에게 똑같은 감정을 줄 수 있습니다. 이러한 컴파일러 및 하드웨어 지원을 통해 정의되지 않은 동작에 대한 이러한 응답은 정의되지 않은 동작의 진정한 의미에 대한 평균 (살아있는) 프로그래머의 이해를 현저하게 증가시킵니다. 즉 표준이 결과 동작에 제약을 가하지 않습니다.

C ++ 14

unsigned char 유형의 unsigned char 값을 사용하면 값을 다음과 같이 사용하면 정의되지 않은 동작이 발생하지 않습니다.

  • 삼항 조건부 연산자의 두 번째 또는 세 번째 피연산자;
  • 내장 된 쉼표 연산자의 오른쪽 피연산자.
  • unsigned char 로의 변환의 피연산자;
  • 왼쪽 피연산자가 unsigned char 유형 인 경우 할당 연산자의 오른쪽 피연산자.
  • unsigned char 객체의 초기화 자.

또는 값이 파기되는 경우. 이러한 경우 indeterminate 값은 적용 가능한 경우 표현식의 결과로 전달됩니다.

static 변수는 항상 0으로 초기화됩니다 (가능한 경우).

static int a;
std::cout << a; // Defined behavior, 'a' is 0

여러 개의 동일하지 않은 정의 (One Definition Rule)

템플릿의 클래스, 열거 형, 인라인 함수, 템플릿 또는 멤버가 외부 연결을 포함하고 여러 번역 단위로 정의 된 경우 모든 정의가 동일해야하며 하나의 정의 규칙 (ODR) 에 따라 동작이 정의되지 않습니다.

foo.h :

class Foo {
  public:
    double x;
  private:
    int y;
};

Foo get_foo();

foo.cpp :

#include "foo.h"
Foo get_foo() { /* implementation */ }

main.cpp :

// I want access to the private member, so I am going to replace Foo with my own type
class Foo {
  public:
    double x;
    int y;
};
Foo get_foo(); // declare this function ourselves since we aren't including foo.h
int main() {
    Foo foo = get_foo();
    // do something with foo.y
}

위의 프로그램은 클래스 ::Foo 의 두 가지 정의가 포함되어 있기 때문에 정의되지 않은 동작을합니다. 다른 변환 단위에는 외부 연결이 있지만 두 정의는 동일하지 않습니다. 동일한 번역 단위 내에서 클래스를 재정의하는 것과 달리이 문제는 컴파일러에서 진단 할 필요가 없습니다.

잘못된 메모리 할당 및 할당 해제

객체는 new 에 의해 할당되고 배열이 아닌 경우에만 delete 의해 할당 해제 될 수 있습니다. delete 인수가 new 로 반환되지 않거나 배열 인 경우 동작은 정의되지 않습니다.

객체는 new 에 의해 할당되고 배열 인 경우 delete[] 에 의해서만 할당 해제 될 수 있습니다. delete[] 에 대한 인수가 new 의해 반환되지 않았거나 배열이 아닌 경우 동작은 정의되지 않습니다.

free 에 대한 인수가 malloc 에 의해 반환되지 않으면 비헤이비어가 정의되지 않습니다.

int* p1 = new int;
delete p1;      // correct
// delete[] p1; // undefined
// free(p1);    // undefined

int* p2 = new int[10];
delete[] p2;    // correct
// delete p2;   // undefined
// free(p2);    // undefined

int* p3 = static_cast<int*>(malloc(sizeof(int)));
free(p3);       // correct
// delete p3;   // undefined
// delete[] p3; // undefined

이러한 문제는 malloc 을 완전히 피하고 C ++ 프로그램에서 free 로 피할 수 있습니다. 원시 라이브러리 newdelete 비해 표준 라이브러리 스마트 포인터를 선호하며 raw newdelete[] 비해 std::vectorstd::string 을 선호합니다.

객체를 잘못된 유형으로 액세스

대부분의 경우 한 유형의 객체에 액세스하는 것이 다른 유형 인 것처럼 위법입니다 (cv 한정자 무시). 예:

float x = 42;
int y = reinterpret_cast<int&>(x);

결과는 정의되지 않은 동작입니다.

엄격한 앨리어싱 규칙에는 몇 가지 예외가 있습니다.

  • 클래스 유형의 오브젝트는 실제 클래스 유형의 기본 클래스 인 것처럼 액세스 할 수 있습니다.
  • 모든 유형은 char 또는 unsigned char 로 액세스 할 수 있지만 그 반대는 사실이 아닙니다. char 배열은 임의의 유형인 것처럼 액세스 할 수 없습니다.
  • 부호있는 정수 유형은 해당 부호없는 유형으로 액세스 할 수 있으며 그 반대의 경우도 가능 합니다.

관련 규칙은 함수의 정의 클래스 또는 파생 클래스와 실제로 같은 유형이 아닌 객체에서 비 정적 멤버 함수가 호출되면 정의되지 않은 동작이 발생한다는 것입니다. 함수가 객체에 액세스하지 않더라도 true입니다.

struct Base {
};
struct Derived : Base {
    void f() {}
};
struct Unrelated {};
Unrelated u;
Derived& r1 = reinterpret_cast<Derived&>(u); // ok
r1.f();                                      // UB
Base b;
Derived& r2 = reinterpret_cast<Derived&>(b); // ok
r2.f();                                      // UB

부동 소수점 오버플로

부동 소수점 유형을 산출하는 산술 연산이 결과 유형의 표현 가능한 값 범위에 속하지 않는 값을 생성하는 경우, C ++ 표준에 따라 동작이 정의되지 않지만 기계가 준수 할 수있는 다른 표준에 의해 정의 될 수 있습니다. IEEE 754와 같은

float x = 1.0;
for (int i = 0; i < 10000; i++) {
    x *= 10.0; // will probably overflow eventually; undefined behavior
}

생성자 또는 소멸자로부터 (순수한) 가상 멤버 호출하기

표준 (10.4) 은 다음과 같이 말합니다 :

멤버 함수는 추상 클래스의 생성자 (또는 소멸자)에서 호출 할 수 있습니다. 그러한 생성자 (또는 소멸자)로부터 생성 (또는 소멸)되는 객체에 대해 직접 또는 간접적으로 순수 가상 함수에 가상 호출 (10.3)을하는 효과는 정의되지 않습니다.

좀 더 일반적으로, Scott Meyers와 같은 일부 C ++ 당국은 생성자와 dstructor에서 결코 가상 함수 (심지어 비 순수 함수)를 호출하지 않을 것을 제안 합니다.

위의 링크에서 수정 된 다음 예제를 고려하십시오.

class transaction
{
public:
    transaction(){ log_it(); }
    virtual void log_it() const = 0;
};

class sell_transaction : public transaction
{
public:
    virtual void log_it() const { /* Do something */ }
};

sell_transaction 객체를 생성한다고 가정 sell_transaction .

sell_transaction s;

이것은 암시 적으로 먼저 transaction 생성자를 호출하는 sell_transaction 의 생성자를 호출합니다. transaction 생성자가 호출되면 객체는 아직 sell_transaction 유형이 아니라 transaction 유형입니다.

결과적으로, transaction::transaction() 에서 log_it 호출은 직관적 인 것으로 보이는 것을하지 않을 것이다. 즉, sell_transaction::log_it 호출한다.

  • 이 예제에서와 같이 log_it 이 순수 가상 인 경우 동작은 정의되지 않습니다.

  • log_it 이 비 순수 가상 transaction::log_it 이 호출 될 것이다.

가상 소멸자가없는 기본 클래스에 대한 포인터를 통해 파생 된 개체를 삭제합니다.

class base { };
class derived: public base { }; 

int main() {
    base* p = new derived();
    delete p; // The is undefined behavior!
}

섹션 [expr.delete] §5.3.5 / 3에서 표준은 virtual 소멸자가없는 정적 유형을 가진 객체에서 delete 가 호출되면 다음과 같이 나타냅니다.

삭제할 객체의 정적 유형이 동적 유형과 다른 경우 정적 유형은 삭제할 객체의 동적 유형의 기본 클래스이고 정적 유형은 가상 소멸자를 가져야하거나 동작이 정의되지 않습니다.

파생 클래스가 기본 클래스에 데이터 멤버를 추가했는지 여부에 관계없이이 경우입니다.

매달려있는 참조에 액세스하기

범위를 벗어난 객체에 대한 참조에 액세스하거나 그렇지 않으면 파괴 된 것은 불법입니다. 그러한 참조는 더 이상 유효한 객체를 참조하지 않으므로 매달려 있다고합니다.

#include <iostream>
int& getX() {
    int x = 42;
    return x;
}
int main() {
    int& r = getX();
    std::cout << r << "\n";
}

이 예제에서 getX 반환되면 지역 변수 x 가 범위를 벗어납니다. ( 평생 확장 은 정의 된 블록의 범위를 지나서 로컬 변수의 수명을 연장 할 수 없습니다. 따라서 r 은 매달린 참조입니다. 작동 및 인쇄 것처럼 보일 수 있지만이 프로그램은 동작을 정의되지 않은 한 42 어떤 경우에.

`std` 또는`posix` 네임 스페이스 확장하기

표준 (17.6.4.2.1 / 1)은 일반적으로 std 네임 스페이스를 확장하는 것을 금지합니다.

C ++ 프로그램의 동작은 별도로 지정하지 않는 한 선언 또는 정의를 네임 스페이스 std 또는 네임 스페이스 std의 네임 스페이스에 추가하면 정의되지 않습니다.

posix (17.6.4.2.2 / 1)도 마찬가지입니다.

별도로 지정하지 않는 한 선언이나 정의를 네임 스페이스 posix 또는 네임 스페이스 posix 내의 네임 스페이스에 추가하면 C ++ 프로그램의 동작이 정의되지 않습니다.

다음을 고려하세요:

#include <algorithm>

namespace std
{
    int foo(){}
}

표준의 어떤 것도 동일한 정의를 정의하는 algorithm (또는 포함하는 헤더 중 하나)을 금지하지 않으므로이 코드는 One Definition Rule을 위반합니다.

그래서, 일반적으로 이것은 금지되어 있습니다. 특정 예외가 허용 됩니다. 아마도 가장 유용하게 사용자 정의 유형에 대한 특수화를 추가 할 수 있습니다. 예를 들어 코드에

class foo
{
    // Stuff
};

다음은 괜찮습니다.

namespace std
{
    template<>
    struct hash<foo>
    {
    public:
        size_t operator()(const foo &f) const;
    };
}

부동 소수점 형으로 또는 부동 소수점 형으로 변환하는 동안 오버플로

다음 중 변환 중에 :

  • 정수형에서 부동 소수점 형으로,
  • 부동 소수점 형에서 정수형으로, 또는
  • 부동 소수점 형을보다 짧은 부동 소수점 형으로,

소스 값이 대상 유형에서 표시 할 수있는 값 범위를 벗어나면 결과는 정의되지 않은 동작입니다. 예:

double x = 1e100;
int y = x; // int probably cannot hold numbers that large, so this is UB

기본에서 파생 클래스로의 정적 캐스트가 잘못되었습니다.

static_cast 를 사용하여 파생 클래스에 대한 포인터 (참조 참조)를 기본 클래스로 포인터 (참조 참조)로 변환하지만 피연산자가 파생 클래스 유형의 객체를 가리 키지 않으면 (참조 참조) 비헤이비어 정의되지 않았습니다. 파생 변환에 대한 기본 사항을 참조하십시오.

일치하지 않는 함수 포인터 유형을 통한 함수 호출

함수 포인터를 통해 함수를 호출하려면 함수 포인터의 유형이 함수의 유형과 정확하게 일치해야합니다. 그렇지 않으면 동작이 정의되지 않습니다. 예:

int f();
void (*p)() = reinterpret_cast<void(*)()>(f);
p(); // undefined

const 객체 수정하기

const 개체를 수정하려고하면 정의되지 않은 동작이 발생합니다. 이 적용 const 변수의 회원 const 개체 및 클래스 멤버는 선언 const . (그러나, mutable (A)의 부재 const 목적은 아니다 const .)

이러한 시도는 const_cast 통해 수행 할 수 있습니다.

const int x = 123;
const_cast<int&>(x) = 456;
std::cout << x << '\n';

컴파일러는 보통의 값 인라인 것 const int 객체를, 그래서이 코드는 컴파일하고 인쇄하는 것이이 가능합니다 123 . 컴파일러는 const 개체의 값을 읽기 전용 메모리에 배치 할 수 있으므로 세분화 오류가 발생할 수 있습니다. 어쨌든, 그 행동은 정의되지 않았고 프로그램은 무엇이든 할 수 있습니다.

다음 프로그램은 훨씬 더 미묘한 오류를 숨 깁니다.

#include <iostream>

class Foo* instance;

class Foo {
  public:
    int get_x() const { return m_x; }
    void set_x(int x) { m_x = x; }
  private:
    Foo(int x, Foo*& this_ref): m_x(x) {
        this_ref = this;
    }
    int m_x;
    friend const Foo& getFoo();
};

const Foo& getFoo() {
    static const Foo foo(123, instance);
    return foo;
}

void do_evil(int x) {
    instance->set_x(x);
}

int main() {
    const Foo& foo = getFoo();
    do_evil(456);
    std::cout << foo.get_x() << '\n';
}

이 코드에서 getFooconst Foo 유형의 싱글 톤을 만들고 m_x 멤버는 123 으로 초기화됩니다. 그런 다음 do_evil 호출되고 값 foo.m_x 분명히 잘못 무엇 456로 변경?

그 이름에도 불구하고, do_evil 은 특별히 악을 do_evil 않습니다. 그 일은 Foo* 통해 세터에게 전화하는 것입니다. const_cast 가 사용되지 않았지만 그 포인터는 const Foo 객체를 가리 킵니다. 이 포인터는 Foo 의 생성자를 통해 가져 Foo . const 객체는 초기화가 완료 될 때까지 const 가되지 않으므로 생성자 내에 const Foo* 아닌 Foo* 유형 this 있습니다.

따라서이 프로그램에 명백하게 위험한 구성이 없더라도 정의되지 않은 동작이 발생합니다.

멤버에 대한 포인터를 통해 존재하지 않는 멤버에 액세스

멤버에 대한 포인터를 통해 객체의 비 정적 멤버에 액세스 할 때 객체가 실제로 포인터로 표시된 멤버를 포함하지 않으면 비헤이비어가 정의되지 않습니다. (이러한 멤버에 대한 포인터는 static_cast 를 통해 얻을 수 있습니다.)

struct Base { int x; };
struct Derived : Base { int y; };
int Derived::*pdy = &Derived::y;
int Base::*pby = static_cast<int Base::*>(pdy);

Base* b1 = new Derived;
b1->*pby = 42; // ok; sets y in Derived object to 42
Base* b2 = new Base;
b2->*pby = 42; // undefined; there is no y member in Base

멤버에 대한 포인터에 대한 파생 - 기반 변환이 잘못되었습니다.

static_cast 를 사용하여 TD::*TB::* 로 변환 할 때 가리키는 멤버는 기본 클래스 또는 B 파생 클래스 인 클래스에 속해야합니다. 그렇지 않으면 동작이 정의되지 않습니다. 자세한 내용 은 멤버에 대한 포인터에 대한 파생 변환을 참조하십시오.

포인터 연산이 잘못되었습니다.

다음 포인터 연산을 사용하면 정의되지 않은 동작이 발생합니다.

  • 결과가 포인터 피연산자와 동일한 배열 객체에 속하지 않는 경우 정수를 더하거나 뺍니다. (여기에서 끝을 지나친 요소 하나는 여전히 배열에 속한 것으로 간주됩니다.)

    int a[10];
    int* p1 = &a[5];
    int* p2 = p1 + 4; // ok; p2 points to a[9]
    int* p3 = p1 + 5; // ok; p2 points to one past the end of a
    int* p4 = p1 + 6; // UB
    int* p5 = p1 - 5; // ok; p2 points to a[0]
    int* p6 = p1 - 6; // UB
    int* p7 = p3 - 5; // ok; p7 points to a[5]
    
  • 둘 다 동일한 배열 객체에 속하지 않는 경우 두 포인터를 뺍니다. (다시 말하면, 끝을 지나친 요소 1은 배열에 속한 것으로 간주됩니다.) 예외적으로 두 개의 널 포인터를 뺄 수 있고 0을 산출 할 수 있습니다.

    int a[10];
    int b[10];
    int *p1 = &a[8], *p2 = &a[3];
    int d1 = p1 - p2; // yields 5
    int *p3 = p1 + 2; // ok; p3 points to one past the end of a
    int d2 = p3 - p2; // yields 7
    int *p4 = &b[0];
    int d3 = p4 - p1; // UB
    
  • 결과가 std::ptrdiff_t 오버플로하는 경우 두 포인터를 뺍니다.

  • 피연산자의 pointee 유형이 가리키는 객체의 동적 유형과 일치하지 않는 모든 포인터 산술 (cv-qualification 무시). 표준에 따르면 "특히 배열에 파생 클래스 유형의 객체가 포함되어있을 때 기본 클래스에 대한 포인터를 포인터 산술에 사용할 수 없습니다."

    struct Base { int x; };
    struct Derived : Base { int y; };
    Derived a[10];
    Base* p1 = &a[1];           // ok
    Base* p2 = p1 + 1;          // UB; p1 points to Derived
    Base* p3 = p1 - 1;          // likewise
    Base* p4 = &a[2];           // ok
    auto p5 = p4 - p1;          // UB; p4 and p1 point to Derived
    const Derived* p6 = &a[1];
    const Derived* p7 = p6 + 1; // ok; cv-qualifiers don't matter
    

유효하지 않은 수의 위치로 이동

내장형 시프트 연산자의 경우 오른쪽 피연산자는 음수가 아니어야하며 승격 된 왼쪽 피연산자의 비트 너비보다 엄격하게 작아야합니다. 그렇지 않으면 동작이 정의되지 않습니다.

const int a = 42;
const int b = a << -1; // UB
const int c = a << 0;  // ok
const int d = a << 32; // UB if int is 32 bits or less
const int e = a >> 32; // also UB if int is 32 bits or less
const signed char f = 'x';
const int g = f << 10; // ok even if signed char is 10 bits or less;
                       // int must be at least 16 bits

[noreturn] 함수에서 돌아 오는 중

C ++ 11

표준 [dcl.attr.noreturn]의 예 :

[[ noreturn ]] void f() {
  throw "error"; // OK
}
[[ noreturn ]] void q(int i) { // behavior is undefined if called with an argument <= 0
  if (i > 0)
    throw "positive";
}

이미 파괴 된 객체를 파기하는 중

이 예제에서 소멸자는 나중에 자동으로 삭제 될 객체에 대해 명시 적으로 호출됩니다.

struct S {
    ~S() { std::cout << "destroying S\n"; }
};
int main() {
    S s;
    s.~S();
} // UB: s destroyed a second time here

자동 또는 정적 저장 기간을 사용하여 T 를 가리키는 std::unique_ptr<T> 이 만들어지면 비슷한 문제가 발생합니다.

void f(std::unique_ptr<S> p);
int main() {
    S s;
    std::unique_ptr<S> p(&s);
    f(std::move(p)); // s destroyed upon return from f
}                    // UB: s destroyed

객체를 두 번 파괴하는 또 다른 방법은 두 개의 shared_ptr 모두 소유권을 공유하지 않고 객체를 관리하는 것입니다.

void f(std::shared_ptr<S> p1, std::shared_ptr<S> p2);
int main() {
    S* p = new S;
    // I want to pass the same object twice...
    std::shared_ptr<S> sp1(p);
    std::shared_ptr<S> sp2(p);
    f(sp1, sp2);
} // UB: both sp1 and sp2 will destroy s separately
// NB: this is correct:
// std::shared_ptr<S> sp(p);
// f(sp, sp);

무한 템플릿 재귀

표준의 예제, [temp.inst] / 17 :

template<class T> class X {
    X<T>* p; // OK
    X<T*> a; // implicit generation of X<T> requires
             // the implicit instantiation of X<T*> which requires
             // the implicit instantiation of X<T**> which ...
};


Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow