Szukaj…


Wprowadzenie

Co to jest niezdefiniowane zachowanie (UB)? Zgodnie z normą ISO C ++ (§ 1.3.24, N4296) jest to „zachowanie, w stosunku do którego ta norma międzynarodowa nie nakłada żadnych wymagań”.

Oznacza to, że gdy program napotka UB, wolno mu robić, co chce. Często oznacza to awarię, ale może po prostu nic nie zrobić, sprawić , że demony wypadną z nosa , a nawet sprawiać wrażenie, że działają poprawnie!

Nie trzeba dodawać, że należy unikać pisania kodu wywołującego UB.

Uwagi

Jeśli program zawiera niezdefiniowane zachowanie, standard C ++ nie nakłada żadnych ograniczeń na jego zachowanie.

  • Może się wydawać, że działa zgodnie z zamierzeniami dewelopera, ale może również powodować awarie lub powodować dziwne wyniki.
  • Zachowanie może się różnić między uruchomieniami tego samego programu.
  • Każda część programu może działać nieprawidłowo, w tym linie poprzedzające linię zawierającą niezdefiniowane zachowanie.
  • Wdrożenie nie jest wymagane do udokumentowania wyniku niezdefiniowanego zachowania.

Implementacja może dokumentować wynik operacji, która powoduje niezdefiniowane zachowanie zgodnie ze standardem, ale program zależny od takiego udokumentowanego zachowania nie jest przenośny.

Dlaczego istnieje niezdefiniowane zachowanie

Intuicyjnie, nieokreślone zachowanie jest uważane za coś złego, ponieważ takich błędów nie można rozwiązać w sposób łaskawy, powiedzmy, za pomocą programów obsługi wyjątków.

Ale pozostawienie nieokreślonego zachowania jest w rzeczywistości integralną częścią obietnicy C ++ „nie płacisz za to, czego nie używasz”. Niezdefiniowane zachowanie pozwala kompilatorowi założyć, że programista wie, co robi, i nie wprowadza kodu, aby sprawdzić błędy wyróżnione w powyższych przykładach.

Znajdowanie i unikanie nieokreślonego zachowania

Niektórych narzędzi można użyć do wykrycia niezdefiniowanego zachowania podczas programowania:

  • Większość kompilatorów ma flagi ostrzegawcze ostrzegające o niektórych przypadkach nieokreślonego zachowania podczas kompilacji.
  • Nowsze wersje gcc i clang zawierają tak zwaną flagę „ -fsanitize=undefined zachowanie niezdefiniowane” ( -fsanitize=undefined ), który sprawdzi niezdefiniowane zachowanie w czasie wykonywania, kosztem wydajności.
  • lint -jak narzędzi może wykonać dokładniejsze niezdefiniowanej analiza zachowania.

Niezdefiniowane, nieokreślone i zdefiniowane w implementacji zachowanie

Ze standardu C ++ 14 (ISO / IEC 14882: 2014) sekcja 1.9 (Wykonanie programu):

  1. Opisy semantyczne w niniejszej Normie Międzynarodowej definiują sparametryzowaną niedeterministyczną maszynę abstrakcyjną. [SKALECZENIE]

  2. Niektóre aspekty i operacje maszyny abstrakcyjnej są opisane w niniejszej Normie Międzynarodowej jako zdefiniowane w implementacji (na przykład sizeof(int) ). Stanowią one parametry abstrakcyjnej maszyny . Każde wdrożenie powinno zawierać dokumentację opisującą jego cechy i zachowanie w tym zakresie. [SKALECZENIE]

  3. Niektóre inne aspekty i operacje maszyny abstrakcyjnej są opisane w niniejszej Normie Międzynarodowej jako nieokreślone (na przykład ocena wyrażeń w nowym inicjalizatorze, jeśli funkcja alokacji nie powiedzie pamięci). Tam, gdzie to możliwe, ten Międzynarodowy Standard określa zestaw dopuszczalnych zachowań. Definiują one niedeterministyczne aspekty abstrakcyjnej maszyny. Instancja abstrakcyjnej maszyny może więc mieć więcej niż jedno możliwe wykonanie dla danego programu i danego wejścia.

  4. Niektóre inne operacje są opisane w niniejszej Normie Międzynarodowej jako niezdefiniowane (lub przykład, efekt próby modyfikacji const obiektu). [ Uwaga : ta Norma Międzynarodowa nie nakłada żadnych wymagań na zachowanie programów, które zawierają niezdefiniowane zachowanie. - notatka końcowa ]

Czytanie lub pisanie za pomocą wskaźnika zerowego

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

Jest to niezdefiniowane zachowanie , ponieważ wskaźnik zerowy nie wskazuje żadnego poprawnego obiektu, więc nie ma obiektu w *ptr do zapisania.

Chociaż najczęściej powoduje to błąd segmentacji, jest niezdefiniowany i wszystko może się zdarzyć.

Brak instrukcji return dla funkcji z niezwróconym typem return

Pominięcie instrukcji return w funkcji, która ma typ return, który nie jest void jest niezdefiniowanym zachowaniem .

int function() {  
    // Missing return statement
} 

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

Większość współczesnych kompilatorów emituje ostrzeżenie w czasie kompilacji dla tego rodzaju nieokreślonego zachowania.


Uwaga: main jest jedynym wyjątkiem od reguły. Jeśli main nie ma instrukcji return , kompilator automatycznie wstawia return 0; dla ciebie, więc można go bezpiecznie pominąć.

Modyfikacja literału łańcuchowego

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

"hello world" to dosłowny ciąg znaków, więc jego modyfikacja daje niezdefiniowane zachowanie.

Inicjalizacja str w powyższym przykładzie została formalnie przestarzała (zaplanowana do usunięcia z przyszłej wersji standardu) w C ++ 03. Wiele kompilatorów przed 2003 r. Może wydać ostrzeżenie o tym (np. Podejrzana konwersja). Po 2003 r. Kompilatory zwykle ostrzegają o przestarzałej konwersji.

C ++ 11

Powyższy przykład jest nielegalny i powoduje diagnostykę kompilatora w C ++ 11 i nowszych. Podobny przykład można skonstruować tak, aby wykazywał niezdefiniowane zachowanie, wyraźnie zezwalając na konwersję typu, na przykład:

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

Dostęp do indeksu poza zakresem

Nie jest zdefiniowane zachowanie dostępu do indeksu, który jest poza zakresem dla tablicy (lub standardowego kontenera biblioteki, ponieważ wszystkie są zaimplementowane przy użyciu surowej tablicy):

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

Dozwolony jest wskaźnik wskazujący na koniec tablicy (w tym przypadku array + 5 ), po prostu nie można jej wyrenderować, ponieważ nie jest to prawidłowy element.

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

Zasadniczo nie można tworzyć wskaźnika poza zakresem. Wskaźnik musi wskazywać element w tablicy lub jeden za końcem.

Dzielenie liczb całkowitych przez zero

int x = 5 / 0;    // Undefined behavior

Dzielenie przez 0 jest matematycznie niezdefiniowane i dlatego ma sens, że jest to zachowanie niezdefiniowane.

Jednak:

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

Większość realizacja realizacji IEEE 754, który określa zmiennoprzecinkowy dzielenie przez zero powrotu NaN (jeśli licznik jest 0.0f ) infinity (jeśli licznik jest dodatnia) lub -infinity (jeśli licznik jest negatywna).

Przepełnienie liczby całkowitej ze znakiem

int x = INT_MAX + 1;

// x can be anything -> Undefined behavior

Jeśli podczas oceny wyrażenia wynik nie jest zdefiniowany matematycznie lub nie mieści się w zakresie reprezentatywnych wartości dla jego typu, zachowanie jest niezdefiniowane.

(C ++ 11 Standard paragraf 5/4)

Jest to jedno z bardziej nieprzyjemnych, ponieważ zwykle daje powtarzalne, nie powodujące awarii zachowanie, więc programiści mogą ulec pokusie polegania na obserwowanym zachowaniu.


Z drugiej strony:

unsigned int x = UINT_MAX + 1;

// x is 0

jest dobrze zdefiniowany, ponieważ:

Nieoznaczone liczby całkowite, zadeklarowane jako niepodpisane, powinny być zgodne z prawami arytmetycznego modułu 2^n gdzie n jest liczbą bitów reprezentujących wartość tego konkretnego rozmiaru liczby całkowitej.

(C ++ 11 Standard paragraf 3.9.1 / 4)

Czasami kompilatory mogą wykorzystywać niezdefiniowane zachowanie i optymalizować

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

Ponieważ nie jest zdefiniowane przepełnienie liczb całkowitych ze znakiem, kompilator może założyć, że może się to nigdy nie zdarzyć, a zatem może zoptymalizować blok „if”

Używanie niezainicjowanej zmiennej lokalnej

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

Powoduje to niezdefiniowane zachowanie , ponieważ a jest niezainicjowane.

Często błędnie twierdzi się, że dzieje się tak, ponieważ wartość jest „nieokreślona” lub „jakakolwiek wartość była wcześniej w tym miejscu pamięci”. Jednak czynność uzyskania dostępu do wartości a w powyższym przykładzie daje zachowanie niezdefiniowane. W praktyce drukowanie „wartości śmieci” jest częstym objawem w tym przypadku, ale jest to tylko jedna z możliwych form niezdefiniowanego zachowania.

Chociaż w praktyce jest to mało prawdopodobne (ponieważ jest zależne od konkretnego wsparcia sprzętowego), kompilator może równie dobrze porazić prądem programatora podczas kompilowania powyższego przykładu kodu. Przy takim kompilatorze i wsparciu sprzętowym taka reakcja na niezdefiniowane zachowanie znacznie zwiększyłaby zrozumienie przez przeciętnego (żywego) programistę prawdziwego znaczenia niezdefiniowanego zachowania - co oznacza, że standard nie nakłada żadnych ograniczeń na wynikowe zachowanie.

C ++ 14

Użycie nieokreślonej wartości unsigned char typu unsigned char nie powoduje niezdefiniowanego zachowania, jeśli wartość jest używana jako:

  • drugi lub trzeci operand trójskładnikowego operatora warunkowego;
  • właściwy operand wbudowanego operatora przecinka;
  • operand konwersji na unsigned char ;
  • prawy operand operatora przypisania, jeśli lewy operand jest również typu unsigned char
  • inicjalizator unsigned char obiektu unsigned char ;

lub jeśli wartość zostanie odrzucona. W takich przypadkach nieokreślona wartość po prostu propaguje się do wyniku wyrażenia, jeśli dotyczy.

Zauważ, że zmienna static jest zawsze inicjowana zerem (jeśli to możliwe):

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

Wiele nieidentycznych definicji (reguła jednej definicji)

Jeśli klasa, wyliczenie, funkcja wstawiana, szablon lub element szablonu ma powiązanie zewnętrzne i jest zdefiniowane w wielu jednostkach tłumaczeniowych, wszystkie definicje muszą być identyczne lub zachowanie jest niezdefiniowane zgodnie z regułą jednej definicji (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
}

Powyższy program wykazuje niezdefiniowane zachowanie, ponieważ zawiera dwie definicje klasy ::Foo , która ma powiązanie zewnętrzne, w różnych jednostkach tłumaczeniowych, ale dwie definicje nie są identyczne. W przeciwieństwie do redefinicji klasy w tej samej jednostce tłumaczeniowej kompilator nie musi diagnozować tego problemu.

Niepoprawne parowanie alokacji pamięci i dezalokacji

Obiekt można zwolnić przez delete jeśli został on przydzielony przez new i nie jest tablicą. Jeśli argument do delete nie został zwrócony przez new lub jest tablicą, zachowanie jest niezdefiniowane.

Obiekt można zwolnić przez delete[] wtedy, gdy został przydzielony przez new i jest tablicą. Jeśli argument, aby delete[] nie został zwrócony przez new lub nie jest tablicą, zachowanie jest niezdefiniowane.

Jeśli argument free nie został zwrócony przez malloc , zachowanie jest niezdefiniowane.

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

Takich problemów można uniknąć, całkowicie unikając malloc i free w programach C ++, preferując standardowe inteligentne wskaźniki biblioteki zamiast surowego new i delete oraz preferując std::vector i std::string zamiast surowego new i delete[] .

Uzyskiwanie dostępu do obiektu jako niewłaściwy typ

W większości przypadków nielegalne jest uzyskiwanie dostępu do obiektu jednego typu, tak jakby był to inny typ (pomijając kwalifikatory cv). Przykład:

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

Rezultatem jest niezdefiniowane zachowanie.

Istnieją pewne wyjątki od tej ścisłej zasady aliasingu :

  • Dostęp do obiektu typu klasy można uzyskać tak, jakby był typu, który jest klasą bazową rzeczywistego typu klasy.
  • Dostęp do dowolnego typu można uzyskać jako char lub unsigned char , ale odwrotna sytuacja nie jest prawdą: nie można uzyskać dostępu do tablicy znaków tak, jakby to był typ dowolny.
  • Dostęp do podpisanego typu liczb całkowitych można uzyskać jako odpowiadający mu typ bez znaku i odwrotnie .

Powiązaną zasadą jest to, że jeśli wywoływana jest niestatyczna funkcja składowa na obiekcie, który w rzeczywistości nie ma tego samego typu co klasa definiująca funkcję lub klasa pochodna, wówczas zachodzi niezdefiniowane zachowanie. Jest to prawdą, nawet jeśli funkcja nie ma dostępu do obiektu.

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

Przepełnienie zmiennoprzecinkowe

Jeśli operacja arytmetyczna, która daje typ zmiennoprzecinkowy, daje wartość, która nie mieści się w zakresie reprezentatywnych wartości typu wynikowego, zachowanie jest niezdefiniowane zgodnie ze standardem C ++, ale może być zdefiniowane przez inne standardy, z którymi maszyna może się zgadzać, takie jak IEEE 754.

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

Wywoływanie (czystych) wirtualnych członków od konstruktora lub niszczyciela

Standard (10.4) stanowi:

Funkcje składowe można wywoływać z konstruktora (lub destruktora) klasy abstrakcyjnej; efekt wywołania wirtualnego (10.3) do funkcji wirtualnej bezpośrednio lub pośrednio dla obiektu tworzonego (lub niszczonego) z takiego konstruktora (lub destruktora) jest nieokreślony.

Mówiąc bardziej ogólnie, niektóre autorytety C ++, np. Scott Meyers, sugerują, aby nigdy nie wywoływać funkcji wirtualnych (nawet nieczystych) z konstruktorów i dstruktorów.

Rozważ następujący przykład, zmodyfikowany z powyższego linku:

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 */ }
};

Załóżmy, że tworzymy obiekt sell_transaction :

sell_transaction s;

To domyślnie wywołuje konstruktora sell_transaction , który najpierw wywołuje konstruktor transaction . Gdy wywoływany jest konstruktor transaction , obiekt nie jest jeszcze typu sell_transaction , a jedynie typu transaction .

W związku z tym wywołanie w transaction::transaction() do log_it nie zrobi tego, co mogłoby się wydawać intuicyjną rzeczą - mianowicie sell_transaction::log_it .

  • Jeśli log_it jest czysto wirtualny, tak jak w tym przykładzie, zachowanie jest niezdefiniowane.

  • Jeśli log_it nie jest wirtualny czysty, zostanie wywołana transaction::log_it .

Usuwanie obiektu pochodnego za pomocą wskaźnika do klasy podstawowej, która nie ma wirtualnego destruktora.

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

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

W sekcji [wyrażenie.delete] §5.3.5 / 3 standard mówi, że jeśli zostanie wywołane delete na obiekcie, którego typ statyczny nie ma virtual destruktora:

jeżeli typ statyczny obiektu, który ma zostać usunięty, różni się od jego typu dynamicznego, typ statyczny powinien być klasą bazową typu dynamicznego obiektu, który ma zostać usunięty, a typ statyczny powinien mieć wirtualny destruktor lub zachowanie jest niezdefiniowane.

Dzieje się tak niezależnie od pytania, czy klasa pochodna dodała jakieś elementy danych do klasy podstawowej.

Dostęp do wiszącej referencji

Dostęp do odniesienia do obiektu, który wykraczał poza zakres lub został w inny sposób zniszczony, jest nielegalny. Takie odniesienie jest powieszone, ponieważ nie odnosi się już do ważnego obiektu.

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

W tym przykładzie lokalna zmienna x wychodzi poza zakres, gdy getX zwraca. (Należy zauważyć, że przedłużenie życia nie może wydłużyć żywotność zmiennej lokalnej ostatniego zakresu bloku, w którym jest zdefiniowana). Dlatego też r jest wiszącym odniesienia. Ten program ma niezdefiniowane zachowanie, chociaż może wydawać się działać i drukować 42 w niektórych przypadkach.

Rozszerzanie przestrzeni nazw `std` lub` posix`

Standard (17.6.4.2.1 / 1) ogólnie zabrania rozszerzania std przestrzeni nazw:

Zachowanie programu C ++ jest niezdefiniowane, jeśli dodaje deklaracje lub definicje do przestrzeni nazw std lub przestrzeni nazw w przestrzeni nazw std, chyba że określono inaczej.

To samo dotyczy posix (17.6.4.2.2 / 1):

Zachowanie programu C ++ jest niezdefiniowane, jeśli dodaje deklaracje lub definicje do POSIX przestrzeni nazw lub przestrzeni nazw w POSIX przestrzeni nazw, chyba że określono inaczej.

Rozważ następujące:

#include <algorithm>

namespace std
{
    int foo(){}
}

Nic w standardowym algorithm zabrania (lub jednego z zawartych w nim nagłówków) definiowania tej samej definicji, a zatem ten kod naruszyłby regułę jednej definicji .

Zasadniczo jest to zabronione. Istnieją jednak szczególne wyjątki . Być może najbardziej przydatne jest dodanie specjalizacji dla typów zdefiniowanych przez użytkownika. Załóżmy na przykład, że Twój kod ma

class foo
{
    // Stuff
};

To jest w porządku

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

Przepełnienie podczas konwersji na lub z typu zmiennoprzecinkowego

Jeżeli podczas konwersji:

  • typ całkowity na typ zmiennoprzecinkowy,
  • typ zmiennoprzecinkowy na typ całkowity lub
  • typ zmiennoprzecinkowy na krótszy typ zmiennoprzecinkowy,

wartość źródłowa jest poza zakresem wartości, które mogą być reprezentowane w typie docelowym, w wyniku czego zachowanie jest niezdefiniowane. Przykład:

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

Nieprawidłowy rzut statyczny na podstawie na podstawie

Jeśli static_cast jest używany do konwersji wskaźnika (lub referencji) do klasy bazowej na wskaźnik (lub referencji) do klasy pochodnej, ale operand nie wskazuje (odpowiednio. Referencji) na obiekt typu klasy pochodnej, zachowanie jest niezdefiniowany. Zobacz Konwersja bazy do pochodnej .

Wywołanie funkcji przez niedopasowany typ wskaźnika funkcji

Aby wywołać funkcję za pomocą wskaźnika funkcji, typ wskaźnika funkcji musi dokładnie odpowiadać typowi funkcji. W przeciwnym razie zachowanie jest niezdefiniowane. Przykład:

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

Modyfikowanie stałego obiektu

Każda próba zmodyfikowania const obiektu powoduje niezdefiniowane zachowanie. Odnosi się to do zmiennych const , elementów const obiektów i członków klasy deklarowanych const . (Jednak mutable element const obiektu nie jest const .)

Taką próbę można wykonać za pomocą const_cast :

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

Kompilator zwykle wstawia wartość const int obiektu, więc możliwe jest, że ten kod się skompiluje i wydrukuje 123 . Kompilatory mogą również umieszczać wartości const obiektów w pamięci tylko do odczytu, więc może wystąpić błąd segmentacji. W każdym razie zachowanie jest niezdefiniowane, a program może zrobić wszystko.

Poniższy program ukrywa o wiele bardziej subtelny błąd:

#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';
}

W tym kodzie getFoo tworzy singleton typu const Foo a jego element m_x jest inicjowany na 123 . Następnie do_evil jest do_evil a wartość foo.m_x najwyraźniej zmienia się na 456. Co poszło nie tak?

Pomimo swojej nazwy, do_evil nie robi nic szczególnie złego; wystarczy zadzwonić do setera za pośrednictwem Foo* . Ale ten wskaźnik wskazuje na const_cast obiekt const Foo mimo że const_cast nie był używany. Ten wskaźnik został uzyskany przez konstruktora Foo . const obiekt nie stać const aż jej inicjalizacji jest kompletna, więc this ma typ Foo* , nie const Foo* , w konstruktorze.

Dlatego zachowuje się niezdefiniowane zachowanie, mimo że w tym programie nie ma oczywiście niebezpiecznych konstrukcji.

Dostęp do nieistniejącego członka poprzez wskaźnik do członka

Podczas uzyskiwania dostępu do niestatycznego elementu obiektu za pomocą wskaźnika do elementu, jeśli obiekt tak naprawdę nie zawiera elementu oznaczonego wskaźnikiem, zachowanie jest niezdefiniowane. (Taki wskaźnik na element członkowski można uzyskać poprzez 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

Niepoprawna konwersja pochodna na podstawową dla wskaźników do członków

Gdy static_cast jest używany do konwersji TD::* na TB::* , wskazany element musi należeć do klasy, która jest klasą bazową lub pochodną klasy B W przeciwnym razie zachowanie jest niezdefiniowane. Zobacz Konwersja pochodna na podstawową dla wskaźników na członków

Nieprawidłowa arytmetyka wskaźnika

Następujące zastosowania arytmetyki wskaźnika powodują niezdefiniowane zachowanie:

  • Dodawanie lub odejmowanie liczby całkowitej, jeśli wynik nie należy do tego samego obiektu tablicy, co operand wskaźnika. (W tym przypadku uważa się, że element jeden za końcem nadal należy do tablicy).

    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]
    
  • Odejmowanie dwóch wskaźników, jeśli nie oba należą do tego samego obiektu tablicy. (Ponownie element jeden za końcem uważa się za należący do tablicy). Wyjątkiem jest odjęcie dwóch wskaźników zerowych, co daje wartość 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
    
  • Odejmowanie dwóch wskaźników, jeśli wynik przepełni std::ptrdiff_t .

  • Dowolna arytmetyka wskaźnika, w której typ pointee dowolnego operandu nie jest zgodny z typem dynamicznym wskazanego obiektu (ignorując kwalifikację cv). Zgodnie ze standardem „[w szczególności] wskaźnik do klasy bazowej nie może być używany do arytmetyki wskaźnika, gdy tablica zawiera obiekty typu pochodnego.”

    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
    

Przesunięcie o nieprawidłową liczbę pozycji

W przypadku wbudowanego operatora zmiany prawy operand musi być nieujemny i ściśle mniejszy niż szerokość bitowa promowanego lewego operandu. W przeciwnym razie zachowanie jest niezdefiniowane.

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

Powrót z funkcji [[noreturn]]

C ++ 11

Przykład ze standardu [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";
}

Niszczenie obiektu, który został już zniszczony

W tym przykładzie bezpośrednio wywoływany jest destruktor dla obiektu, który później zostanie automatycznie zniszczony.

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

Podobny problem występuje, gdy parametr std::unique_ptr<T> jest wskazywany na T z automatycznym lub statycznym czasem przechowywania.

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

Innym sposobem, aby zniszczyć obiekt jest dwa razy przez posiadające dwa shared_ptr s zarówno zarządzać przedmiot własności bez dzielenia się ze sobą.

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);

Nieskończona rekurencja szablonu

Przykład ze standardu, [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
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow