Zoeken…


Invoering

Wat is ongedefinieerd gedrag (UB)? Volgens de ISO C ++ standaard (§1.3.24, N4296) is het "gedrag waarvoor deze internationale norm geen eisen stelt."

Dit betekent dat wanneer een programma UB tegenkomt, het is toegestaan om te doen wat het wil. Dit betekent vaak een crash, maar het kan gewoon niets doen, demonen uit je neus laten vliegen of zelfs goed lijken te werken!

Onnodig te zeggen, moet u vermijden om code te schrijven die UB oproept.

Opmerkingen

Als een programma ongedefinieerd gedrag bevat, stelt de C ++ standaard geen beperkingen aan zijn gedrag.

  • Het lijkt misschien te werken zoals de ontwikkelaar het bedoelde, maar het kan ook crashen of vreemde resultaten opleveren.
  • Het gedrag kan variëren tussen runs van hetzelfde programma.
  • Elk onderdeel van het programma kan defect raken, inclusief lijnen die voor de regel komen die ongedefinieerd gedrag bevat.
  • De implementatie is niet vereist om het resultaat van ongedefinieerd gedrag te documenteren.

Een implementatie kan het resultaat van een bewerking documenteren die ongedefinieerd gedrag volgens de standaard produceert, maar een programma dat afhankelijk is van dergelijk gedocumenteerd gedrag is niet draagbaar.

Waarom ongedefinieerd gedrag bestaat

Intuïtief wordt ongedefinieerd gedrag als een slechte zaak beschouwd, omdat dergelijke fouten niet genadig kunnen worden afgehandeld via bijvoorbeeld uitzonderingshandlers.

Maar wat gedrag ongedefinieerd laat, is eigenlijk een integraal onderdeel van de belofte van C ++ "u betaalt niet voor wat u niet gebruikt". Door ongedefinieerd gedrag kan een compiler aannemen dat de ontwikkelaar weet wat hij doet en geen code invoeren om te controleren op de fouten die in de bovenstaande voorbeelden zijn gemarkeerd.

Ongedefinieerd gedrag vinden en vermijden

Sommige tools kunnen worden gebruikt om ongedefinieerd gedrag te ontdekken tijdens de ontwikkeling:

  • De meeste compilers hebben waarschuwingsvlaggen om te waarschuwen voor sommige gevallen van ongedefinieerd gedrag tijdens het compileren.
  • Nieuwere versies van gcc en clang bevatten een zogenaamde 'Undefined Behavior Sanitizer'-vlag ( -fsanitize=undefined ) die tijdens runtime op ongedefinieerd gedrag zal controleren, tegen prestatiekosten.
  • lint -achtige instrumenten kunnen grondiger onbepaald gedrag analyses uit te voeren.

Ongedefinieerd, niet - gespecificeerd en door de implementatie gedefinieerd gedrag

Van C ++ 14 standaard (ISO / IEC 14882: 2014) sectie 1.9 (Programma-uitvoering):

  1. De semantische beschrijvingen in deze internationale norm definiëren een geparametriseerde niet-deterministische abstracte machine. [BESNOEIING]

  2. Bepaalde aspecten en bewerkingen van de abstracte machine worden in deze internationale standaard beschreven als door de implementatie gedefinieerd (bijvoorbeeld sizeof(int) ). Deze vormen de parameters van de abstracte machine . Elke implementatie bevat documentatie die de kenmerken en het gedrag in deze opzichten beschrijft. [BESNOEIING]

  3. Bepaalde andere aspecten en bewerkingen van de abstracte machine worden in deze internationale standaard als niet-gespecificeerd beschreven (bijvoorbeeld evaluatie van uitdrukkingen in een nieuwe initialisatie-eenheid als de toewijzingsfunctie geen geheugen toewijst). Waar mogelijk definieert deze internationale norm een reeks toegestane gedragingen. Deze bepalen de niet-deterministische aspecten van de abstracte machine. Een exemplaar van de abstracte machine kan dus meer dan één mogelijke uitvoering hebben voor een bepaald programma en een gegeven invoer.

  4. Bepaalde andere bewerkingen worden in deze internationale standaard beschreven als ongedefinieerd (of bijvoorbeeld het effect van een poging om een const object te wijzigen). [ Opmerking : deze internationale norm stelt geen eisen aan het gedrag van programma's die ongedefinieerd gedrag bevatten. - eindnoot ]

Lezen of schrijven via een lege wijzer

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

Dit is ongedefinieerd gedrag , omdat een nulwijzer niet naar een geldig object verwijst, dus er is geen object op *ptr om naar te schrijven.

Hoewel dit meestal een segmentatiefout veroorzaakt, is het niet gedefinieerd en kan er van alles gebeuren.

Geen retourinstructie voor een functie met een niet-ongeldig retourtype

Weglaten van de return instructie in een functie die een return type dat niet void is onbepaald gedrag.

int function() {  
    // Missing return statement
} 

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

De meeste moderne compilers geven tijdens het compileren een waarschuwing voor dit soort ongedefinieerd gedrag.


Opmerking: main is de enige uitzondering op de regel. Als main geen return instructie heeft, voegt de compiler automatisch return 0; voor u, zodat het veilig kan worden weggelaten.

Letterlijke tekenreeks wijzigen

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

"hello world" is een letterlijke tekenreeks, dus wijzigen geeft ongedefinieerd gedrag.

De initialisatie van str in het bovenstaande voorbeeld is formeel verouderd (gepland voor verwijdering uit een toekomstige versie van de standaard) in C ++ 03. Een aantal compilers vóór 2003 kunnen hier een waarschuwing voor geven (bijvoorbeeld een verdachte conversie). Na 2003 waarschuwen compilers meestal voor een verouderde conversie.

C ++ 11

Het bovenstaande voorbeeld is illegaal en resulteert in een compiler-diagnose in C ++ 11 en hoger. Een soortgelijk voorbeeld kan worden geconstrueerd om ongedefinieerd gedrag te vertonen door de typeconversie expliciet toe te staan, zoals:

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

Toegang tot een out-of-bounds index

Het is ongedefinieerd gedrag om toegang te krijgen tot een index die buiten het bereik van een array valt (of wat dat betreft standaardbibliotheekcontainer, omdat ze allemaal worden geïmplementeerd met een onbewerkte array):

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

Het is toegestaan om een aanwijzer te hebben die naar het einde van de array wijst (in dit geval array + 5 ), je kunt er gewoon niet van afleiden, omdat het geen geldig element is.

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

Over het algemeen is het niet toegestaan om een aanwijzer buiten de grenzen te maken. Een aanwijzer moet naar een element in de array wijzen of naar een element voorbij het einde.

Geheel getal gedeeld door nul

int x = 5 / 0;    // Undefined behavior

Deling door 0 is wiskundig ongedefinieerd en daarom is het logisch dat dit ongedefinieerd gedrag is.

Echter:

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

De meeste implementaties implementeren IEEE-754, die drijvende komma deling door nul definieert om NaN te retourneren (als teller 0.0f ), infinity (als teller positief is) of -infinity (als teller negatief is).

Getekende integeroverloop

int x = INT_MAX + 1;

// x can be anything -> Undefined behavior

Als tijdens de evaluatie van een uitdrukking het resultaat niet wiskundig is gedefinieerd of niet binnen het bereik van representatieve waarden voor het type valt, is het gedrag niet gedefinieerd.

(C ++ 11 Standaardparagraaf 5/4)

Dit is een van de meest vervelende, omdat het meestal reproduceerbaar, niet-crashend gedrag oplevert, zodat ontwikkelaars in de verleiding kunnen komen om sterk te vertrouwen op het waargenomen gedrag.


Aan de andere kant:

unsigned int x = UINT_MAX + 1;

// x is 0

is goed gedefinieerd sinds:

Niet-ondertekende gehele getallen, niet-ondertekende verklaard, houden zich aan de wetten van de rekenkundige modulo 2^n waarbij n het aantal bits is in de waardeweergave van die bepaalde grootte van het gehele getal.

(C ++ 11 Standaardparagraaf 3.9.1 / 4)

Soms kunnen compilers een ongedefinieerd gedrag gebruiken en optimaliseren

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

Aangezien een ondertekende integer-overflow niet is gedefinieerd, is de compiler vrij om aan te nemen dat dit nooit kan gebeuren en daarom kan het het "if" -blok optimaliseren

Een niet-geïnitialiseerde lokale variabele gebruiken

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

Dit resulteert in ongedefinieerd gedrag , omdat a niet geïnitialiseerd is.

Er wordt vaak ten onrechte beweerd dat dit komt omdat de waarde "onbepaald" is, of "welke waarde er zich eerder in die geheugenlocatie bevond". Het is echter de handeling van toegang tot de waarde van a in het bovenstaande voorbeeld die ongedefinieerd gedrag geeft. In de praktijk is het afdrukken van een 'afvalwaarde' een veel voorkomend symptoom in dit geval, maar dat is slechts een mogelijke vorm van ongedefinieerd gedrag.

Hoewel het in de praktijk zeer onwaarschijnlijk is (omdat het afhankelijk is van specifieke hardware-ondersteuning), kan de compiler net zo goed de programmeur elektrocuteren bij het compileren van het bovenstaande codevoorbeeld. Met een dergelijke compiler en hardware-ondersteuning, zou een dergelijke reactie op ongedefinieerd gedrag de gemiddelde (levende) programmeur begrip van de ware betekenis van ongedefinieerd gedrag aanzienlijk vergroten, wat betekent dat de standaard geen beperkingen oplegt aan het resulterende gedrag.

C ++ 14

Het gebruik van een onbepaalde waarde van het type unsigned char geeft geen ongedefinieerd gedrag als de waarde wordt gebruikt als:

  • de tweede of derde operand van de ternaire voorwaardelijke operator;
  • de juiste operand van de ingebouwde komma-operator;
  • de operand van een conversie naar unsigned char ;
  • de rechteroperand van de toewijzingsoperator, als de linkeroperand ook van het type unsigned char ;
  • de initialisatie voor een unsigned char tekenobject;

of als de waarde wordt weggegooid. In dergelijke gevallen propageert de onbepaalde waarde eenvoudig naar het resultaat van de uitdrukking, indien van toepassing.

Merk op dat een static variabele altijd door nul wordt geïnitialiseerd (indien mogelijk):

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

Meerdere niet-identieke definities (de One Definition Rule)

Als een klasse, opsomming, inline-functie, sjabloon of lid van een sjabloon externe koppeling heeft en is gedefinieerd in meerdere vertaaleenheden, moeten alle definities identiek zijn of het gedrag is niet gedefinieerd volgens de 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
}

Het bovenstaande programma vertoont ongedefinieerd gedrag omdat het twee definities van de klasse bevat ::Foo , die externe koppeling heeft, in verschillende vertaaleenheden, maar de twee definities zijn niet identiek. In tegenstelling tot de herdefiniëring van een klasse binnen dezelfde vertaaleenheid, hoeft dit probleem niet door de compiler te worden gediagnosticeerd.

Onjuiste koppeling van geheugentoewijzing en deallocatie

Een object kan alleen worden delete door delete als het door new is toegewezen en geen array is. Als het te delete argument niet door new is geretourneerd of een array is, is het gedrag niet gedefinieerd.

Een object kan alleen worden delete[] door delete[] als het is toegewezen door new en een array is. Als het argument om delete[] te delete[] niet door new is geretourneerd of geen array is, is het gedrag niet gedefinieerd.

Als het argument om te free niet door malloc is teruggestuurd, is het gedrag niet gedefinieerd.

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

Dergelijke problemen kunnen worden vermeden door malloc en free in C ++ -programma's volledig te vermijden, de standaardbibliotheek slimme aanwijzers te verkiezen boven onbewerkte new en delete , en de voorkeur te geven aan std::vector en std::string boven onbewerkte new en delete[] .

Toegang krijgen tot een object als het verkeerde type

In de meeste gevallen is het illegaal om toegang te krijgen tot een object van het ene type alsof het een ander type is (cv-kwalificaties buiten beschouwing gelaten). Voorbeeld:

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

Het resultaat is ongedefinieerd gedrag.

Er zijn enkele uitzonderingen op deze strikte aliasregel :

  • U kunt een object van het klasse-type benaderen alsof het een type is dat een basisklasse is van het daadwerkelijke klasse-type.
  • Elk type kan worden gebruikt als een char of unsigned char , maar het omgekeerde is niet waar: een char array kan niet worden benaderd alsof het een willekeurig type is.
  • Een getekend geheel getaltype is toegankelijk als het overeenkomstige niet-ondertekende type en vice versa .

Een gerelateerde regel is dat als een niet-statische lidfunctie wordt aangeroepen op een object dat niet hetzelfde type heeft als de definiërende klasse van de functie, of een afgeleide klasse, er dan ongedefinieerd gedrag optreedt. Dit geldt zelfs als de functie geen toegang tot het object heeft.

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

Overloop drijvend punt

Als een rekenkundige bewerking die een type met drijvende komma oplevert, een waarde oplevert die niet binnen het bereik van de representatieve waarden van het resultaattype valt, is het gedrag niet gedefinieerd volgens de C ++ standaard, maar kan het worden gedefinieerd door andere normen waaraan de machine zou kunnen voldoen, zoals IEEE 754.

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

Bellen van (pure) virtuele leden van constructor of destructor

De standaard (10.4) bepaalt:

Lidfuncties kunnen worden aangeroepen vanuit een constructor (of destructor) van een abstracte klasse; het effect van het maken van een virtuele aanroep (10.3) naar een pure virtuele functie direct of indirect voor het object dat wordt gemaakt (of vernietigd) van een dergelijke constructor (of destructor) is niet gedefinieerd.

Meer in het algemeen stellen sommige C ++ -autoriteiten, bijvoorbeeld Scott Meyers, voor om nooit virtuele functies (zelfs niet-pure) van constructors en dstructors aan te roepen.

Beschouw het volgende voorbeeld, gewijzigd via de bovenstaande link:

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

Stel dat we een sell_transaction object maken:

sell_transaction s;

Dit roept impliciet de constructor van sell_transaction , die eerst de constructor van transaction aanroept. Wanneer de constructor van de transaction wordt aangeroepen, is het object nog niet van het type sell_transaction , maar eerder van het type transaction .

Bijgevolg zal de aanroep in transaction::transaction() naar log_it niet doen wat het intuïtieve lijkt te zijn - namelijk call sell_transaction::log_it .

  • Als log_it puur virtueel is, zoals in dit voorbeeld, is het gedrag niet gedefinieerd.

  • Als log_it niet-pure virtueel is, wordt transaction::log_it aangeroepen.

Een afgeleid object via een pointer verwijderen naar een basisklasse die geen virtuele destructor heeft.

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

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

In sectie [expr.delete] §5.3.5 / 3 zegt de norm dat als delete wordt aangeroepen op een object waarvan het statische type geen virtual destructor heeft:

Als het statische type van het te verwijderen object verschilt van het dynamische type, moet het statische type een basisklasse zijn van het dynamische type van het object dat moet worden verwijderd en moet het statische type een virtuele destructor hebben of is het gedrag ongedefinieerd.

Dit is het geval ongeacht de vraag of de afgeleide klasse gegevensleden aan de basisklasse heeft toegevoegd.

Toegang tot een bengelende referentie

Het is illegaal om toegang te krijgen tot een verwijzing naar een object dat buiten bereik is of anderszins is vernietigd. Er wordt gezegd dat een dergelijke verwijzing bungelt, omdat deze niet langer verwijst naar een geldig object.

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

In dit voorbeeld wordt de lokale variabele x buiten bereik als getX terugkeert. (Merk op dat de levensduurverlenging de levensduur van een lokale variabele niet kan verlengen voorbij het bereik van het blok waarin deze is gedefinieerd.) Daarom is r een bengelende referentie. Dit programma heeft een ongedefinieerd gedrag, hoewel het in sommige gevallen lijkt te werken en 42 af te drukken.

De naamruimte `std` of` posix` uitbreiden

De standaard (17.6.4.2.1 / 1) verbiedt in het algemeen het uitbreiden van de std naamruimte:

Het gedrag van een C ++ -programma is niet gedefinieerd als het declaraties of definities toevoegt aan namespace std of aan een namespace binnen namespace std, tenzij anders gespecificeerd.

Hetzelfde geldt voor posix (17.6.4.2.2 / 1):

Het gedrag van een C ++ -programma is niet gedefinieerd als het declaraties of definities toevoegt aan naamruimte posix of aan een naamruimte binnen naamruimte posix, tenzij anders gespecificeerd.

Stel je de volgende situatie voor:

#include <algorithm>

namespace std
{
    int foo(){}
}

Niets in het standaard verbiedt algorithm (of een van de headers die het bevat) die dezelfde definitie definieert, en daarom zou deze code de One Definition-regel overtreden.

In het algemeen is dit dus verboden. Er zijn echter specifieke uitzonderingen toegestaan . Misschien wel het meest nuttig, is het toegestaan om specialisaties toe te voegen voor door de gebruiker gedefinieerde types. Stel bijvoorbeeld dat uw code dat heeft

class foo
{
    // Stuff
};

Dan is het volgende prima

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

Overloop tijdens conversie van of naar drijvende komma type

Als tijdens de conversie van:

  • een geheel getal naar een drijvend punttype,
  • een drijvende-komma tot een geheel getal, of
  • een drijvend punttype naar een korter drijvend punttype,

de bronwaarde valt buiten het bereik van waarden dat kan worden weergegeven in het doeltype, het resultaat is ongedefinieerd gedrag. Voorbeeld:

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

Ongeldige basis-tot-afgeleide statische cast

Als static_cast wordt gebruikt om een pointer (resp. Referentie) naar basisklasse om te zetten in een pointer (resp. Referentie) naar afgeleide klasse, maar de operand verwijst (resp. Verwijs) niet naar een object van het afgeleide klassetype, het gedrag is niet gedefinieerd. Zie Basis naar afgeleide conversie .

Functieaanroep via niet-overeenkomende functiepointer type

Om een functie via een functie-aanwijzer aan te roepen, moet het type van de functie-aanwijzer exact overeenkomen met het type van de functie. Anders is het gedrag niet gedefinieerd. Voorbeeld:

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

Een const-object wijzigen

Elke poging om een const object te wijzigen, resulteert in ongedefinieerd gedrag. Dit is van toepassing op const variabelen, leden van const objecten en klassenleden die const verklaard. (Een mutable lid van een const object is echter geen const .)

Een dergelijke poging kan worden gedaan via const_cast :

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

Een compiler lijnt meestal de waarde van een const int object in, dus het is mogelijk dat deze code 123 compileert en afdrukt. Compilers kunnen de waarden van const objecten ook in het alleen-lezen geheugen plaatsen, waardoor een segmentatiefout kan optreden. In elk geval is het gedrag niet gedefinieerd en kan het programma alles doen.

Het volgende programma verbergt een veel subtielere fout:

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

In deze code maakt getFoo een singleton van het type const Foo en het lid m_x wordt geïnitialiseerd op 123 . Dan wordt do_evil genoemd en de waarde van foo.m_x is blijkbaar gewijzigd in 456. Wat is er misgegaan?

Ondanks zijn naam doet do_evil niets bijzonder slecht; alles wat het doet is een setter bellen via een Foo* . Maar die aanwijzer const_cast naar een const Foo object, hoewel const_cast niet is gebruikt. Deze aanwijzer is verkregen via de constructor van Foo . Een const object wordt pas const als de initialisatie voltooid is, dus this heeft het type Foo* , en niet const Foo* , binnen de constructor.

Daarom treedt ongedefinieerd gedrag op, ook al zijn er geen duidelijk gevaarlijke constructen in dit programma.

Toegang tot niet-bestaand lid via aanwijzer naar lid

Bij toegang tot een niet-statisch lid van een object via een aanwijzer naar een lid, als het object niet het lid bevat dat wordt aangeduid door de aanwijzer, is het gedrag niet gedefinieerd. (Zo'n pointer naar lid kan worden verkregen via 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

Ongeldige conversie afgeleid naar basis voor verwijzingen naar leden

Wanneer static_cast wordt gebruikt om TD::* te converteren naar TB::* , moet het aangegeven lid behoren tot een klasse die een basisklasse of afgeleide klasse van B . Anders is het gedrag niet gedefinieerd. Zie Afgeleide conversie voor verwijzingen naar leden

Ongeldige aanwijzer rekenkunde

Het volgende gebruik van pointer rekenkundig veroorzaakt ongedefinieerd gedrag:

  • Optellen of aftrekken van een geheel getal, als het resultaat niet tot hetzelfde arrayobject behoort als de aanwijzeroperand. (Hier wordt het element aan het einde nog steeds beschouwd als onderdeel van de array.)

    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]
    
  • Aftrekken van twee pointers als ze niet allebei tot hetzelfde arrayobject behoren. (Nogmaals, het element één voorbij het einde wordt beschouwd als behorend tot de array.) De uitzondering is dat twee nulwijzers kunnen worden afgetrokken, wat 0 oplevert.

    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
    
  • Aftrekken van twee aanwijzers als het resultaat overloopt std::ptrdiff_t .

  • Elke aanwijzerberekening waarbij het pointee-type van een operand niet overeenkomt met het dynamische type van het object waarnaar wordt verwezen (cv-kwalificatie wordt genegeerd). Volgens de standaard "kan met name een pointer naar een basisklasse niet worden gebruikt voor rekenkundige aanwijzer wanneer de array objecten van een afgeleid klassetype bevat."

    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
    

Verschuiven met een ongeldig aantal posities

Voor de ingebouwde shift-operator moet de rechteroperand niet-negatief zijn en strikt kleiner zijn dan de bitbreedte van de gepromote linkeroperand. Anders is het gedrag niet gedefinieerd.

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

Terugkeren van een [[noreturn]] functie

C ++ 11

Voorbeeld uit de standaard, [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";
}

Een object vernietigen dat al is vernietigd

In dit voorbeeld wordt een destructor expliciet aangeroepen voor een object dat later automatisch wordt vernietigd.

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

Een soortgelijk probleem doet zich voor wanneer een std::unique_ptr<T> wordt gemaakt om te wijzen op een T met automatische of statische opslagduur.

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

Een andere manier om een object twee keer te vernietigen, is door twee shared_ptr 's beide het object te laten beheren zonder het eigendom met elkaar te delen.

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

Oneindige sjabloon recursie

Voorbeeld uit de standaard, [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
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow