C++
Ongedefinieerd gedrag
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):
De semantische beschrijvingen in deze internationale norm definiëren een geparametriseerde niet-deterministische abstracte machine. [BESNOEIING]
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]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.
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
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.
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
waarbijn
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.
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
ofunsigned 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, wordttransaction::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
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 ...
};