C++
Odefinierat beteende
Sök…
Introduktion
Vad är odefinierat beteende (UB)? Enligt ISO C ++ -standarden (§1.3.24, N4296) är det "beteende för vilket denna internationella standard inte ställer några krav."
Detta innebär att när ett program möter UB är det tillåtet att göra vad det vill. Detta betyder ofta en krasch, men det kan helt enkelt inte göra någonting, få demoner att flyga ut ur näsan eller till och med verkar fungera ordentligt!
Naturligtvis bör du undvika att skriva kod som åberopar UB.
Anmärkningar
Om ett program innehåller odefinierat beteende placerar C ++ -standarden inga begränsningar för dess beteende.
- Det kan tyckas fungera som utvecklaren avsåg, men det kan också krascha eller ge konstiga resultat.
- Beteendet kan variera mellan körningar av samma program.
- Varje del av programmet kan fungera, inklusive rader som kommer före linjen som innehåller odefinierat beteende.
- Implementeringen krävs inte för att dokumentera resultatet av odefinierat beteende.
En implementering kan dokumentera resultatet av en operation som producerar odefinierat beteende enligt standarden, men ett program som beror på sådant dokumenterat beteende är inte portabelt.
Varför odefinierat beteende finns
Intuitivt betraktas odefinierat beteende som en dålig sak eftersom sådana fel inte kan hanteras nådigt genom, säger, undantagshanterare.
Men att lämna lite beteende otydligt är faktiskt en integrerad del av C ++: s löfte "du betalar inte för det du inte använder". Odefinierat beteende tillåter en kompilator att anta att utvecklaren vet vad han gör och inte införa kod för att kontrollera om de misstag som framhävs i exemplen ovan.
Hitta och undvika odefinierat beteende
Vissa verktyg kan användas för att upptäcka odefinierat beteende under utveckling:
- De flesta sammanställare har varningsflaggor för att varna om vissa fall av odefinierat beteende vid sammanställningstiden.
- Nyare versioner av gcc och clang inkluderar en så kallad "Undefined Behavior Sanitizer" -flagga (
-fsanitize=undefined
) som kommer att kontrollera för odefinierat beteende vid körning, till en prestandakostnad. -
lint
-liknande verktyg kan utföra mer noggrann odefinierad beteendeanalys.
Udefinierat, ospecificerat och implementeringsdefinerat beteende
Från C ++ 14-standarden (ISO / IEC 14882: 2014) avsnitt 1.9 (Exekvering av program):
De semantiska beskrivningarna i denna internationella standard definierar en parametrerad nondeterministisk abstrakt maskin. [SKÄRA]
Vissa aspekter och funktioner hos den abstrakta maskinen beskrivs i denna internationella standard som implementeringsdefinerad (till exempel
sizeof(int)
). Dessa utgör parametrarna för den abstrakta maskinen . Varje implementering ska innehålla dokumentation som beskriver dess egenskaper och beteenden i dessa avseenden. [SKÄRA]Vissa andra aspekter och funktioner hos den abstrakta maskinen beskrivs i denna internationella standard som ospecificerade (till exempel utvärdering av uttryck i en nyinitierare om allokeringsfunktionen inte tilldelar minne). Om möjligt definierar denna internationella standard en uppsättning tillåtna beteenden. Dessa definierar de nondeterministiska aspekterna av den abstrakta maskinen. En instans av den abstrakta maskinen kan således ha mer än en möjlig exekvering för ett givet program och en given ingång.
Vissa andra operationer beskrivs i denna internationella standard som odefinierade (eller exempel, effekten av att försöka modifiera ett
const
objekt). [ Obs : denna internationella standard ställer inga krav på beteendet hos program som innehåller odefinierat beteende. - slutanteckning ]
Läsa eller skriva genom en nollpekare
int *ptr = nullptr;
*ptr = 1; // Undefined behavior
Detta är odefinierat beteende , eftersom en nollpekare inte pekar på något giltigt objekt, så det finns inget objekt på *ptr
att skriva till.
Även om detta oftast orsakar ett segmenteringsfel, är det odefinierat och allt kan hända.
Inget returrätt för en funktion med en icke-ogiltig returtyp
Utelämna return
uttalande en funktion som är har en returtyp som inte är void
är odefinierad beteende.
int function() {
// Missing return statement
}
int main() {
function(); //Undefined Behavior
}
De flesta moderna kompilatorer avger en varning vid sammanställningstiden för denna typ av odefinierat beteende.
Obs: main
är det enda undantaget från regeln. Om main
inte har en return
uttalande kompilatorn automatiskt skär return 0;
för dig, så det kan vara säkert utelämnat.
Ändra en strängbokstav
char *str = "hello world";
str[0] = 'H';
"hello world"
är en sträng bokstavlig, så att ändra det ger odefinierat beteende.
Initieringen av str
i exemplet ovan avskrivs formellt (planerat för borttagning från en framtida version av standarden) i C ++ 03. Ett antal kompilatorer före 2003 kan ge en varning om detta (t.ex. en misstänkt konvertering). Efter 2003 varnar kompilatorer vanligtvis för en avskrivad konvertering.
Ovanstående exempel är olagligt och resulterar i en kompilatordiagnostik i C ++ 11 och senare. Ett liknande exempel kan konstrueras för att uppvisa odefinierat beteende genom att uttryckligen tillåta typkonvertering, såsom:
char *str = const_cast<char *>("hello world");
str[0] = 'H';
Åtkomst till ett index utanför gränserna
Det är odefinierat beteende att få tillgång till ett index som är utanför gränserna för en matris (eller standardbibliotekcontainer för den delen, eftersom de alla implementeras med en rå matris):
int array[] = {1, 2, 3, 4, 5};
array[5] = 0; // Undefined behavior
Det är tillåtet att ha en pekare som pekar mot slutet av matrisen (i det här fallet array + 5
), du kan bara inte eliminera det, eftersom det inte är ett giltigt element.
const int *end = array + 5; // Pointer to one past the last index
for (int *p = array; p != end; ++p)
// Do something with `p`
I allmänhet får du inte skapa en pekare utanför gränserna. En pekare måste peka på ett element i matrisen eller en förbi slutet.
Heltalsdelning med noll
int x = 5 / 0; // Undefined behavior
Uppdelning med 0
är matematiskt odefinierat, och som sådan är det vettigt att detta är odefinierat beteende.
Dock:
float x = 5.0f / 0.0f; // x is +infinity
De flesta implementeringsimplementeringar IEEE-754, som definierar flytande punktdelning med noll för att returnera NaN
(om 0.0f
är 0.0f
), infinity
(om teller är positiv) eller- -infinity
(om teller är negativ).
Signerat heltal överflöde
int x = INT_MAX + 1;
// x can be anything -> Undefined behavior
Om resultatet under utvärderingen av ett uttryck inte definieras matematiskt eller inte ligger inom intervallet för representabla värden för dess typ, är beteendet odefinierat.
(C ++ 11 Standardparagraf 5/4)
Detta är en av de mer otäcka, eftersom det vanligtvis ger reproducerbart, icke-kraschande beteende så att utvecklare kan frestas att lita starkt på det observerade beteendet.
Å andra sidan:
unsigned int x = UINT_MAX + 1;
// x is 0
är väl definierad eftersom:
Osignerade heltal, förklarade osignerade, ska följa lagarna i aritmetisk modul
2^n
därn
är antalet bitar i värdespresentationen av den specifika heltalens storlek.
(C ++ 11 Standardparagraf 3.9.1 / 4)
Ibland kan kompilatorer utnyttja ett odefinierat beteende och optimera
signed int x ;
if(x > x + 1)
{
//do something
}
Eftersom ett signerat heltalöverskridning inte definieras, är kompilatorn fritt att anta att det aldrig kan hända och därmed kan det optimera bort "if" -blocket
Med hjälp av en oinitialiserad lokal variabel
int a;
std::cout << a; // Undefined behavior!
Detta resulterar i odefinierat beteende , eftersom a
initialiseras.
Det hävdas ofta, felaktigt, att detta beror på att värdet är "obestämd", eller "vilket värde som var på den minnesplatsen förut". Det är dock handlingen att få tillgång till värdet på a
i exemplet ovan som ger odefinierat beteende. I praktiken är att skriva ut ett "sopvärde" ett vanligt symptom i detta fall, men det är bara en möjlig form av odefinierat beteende.
Även om det är mycket osannolikt i praktiken (eftersom det är beroende av specifikt hårdvarusupport) kan kompilatorn lika väl elektrokulera programmeraren när han sammanställer kodprovet ovan. Med en sådan kompilator- och hårdvarosupport skulle ett sådant svar på odefinierat beteende markant öka den genomsnittliga (levande) programmerarens förståelse av den verkliga betydelsen av odefinierat beteende - vilket är att standarden inte sätter någon begränsning för det resulterande beteendet.
Att använda ett obestämt värde av unsigned char
typ ger inte odefinierat beteende om värdet används som:
- den andra eller tredje operand av den ternära villkorade operatören;
- rätt operand för den inbyggda kommaoperatören;
- operand för en omvandling till
unsigned char
; - den högra operanden för uppdragsoperatören, om den vänstra operand också är av typen
unsigned char
; - initialiseraren för ett
unsigned char
;
eller om värdet tas bort. I sådana fall förökas det obestämda värdet helt enkelt till resultatet av uttrycket, om tillämpligt.
Observera att en static
variabel alltid är nollinitierad (om möjligt):
static int a;
std::cout << a; // Defined behavior, 'a' is 0
Flera icke-identiska definitioner (One Definition-regeln)
Om en klass, enum, inline-funktion, mall eller medlem av en mall har extern länkning och definieras i flera översättningsenheter, måste alla definitioner vara identiska eller beteendet är odefinierat enligt 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
}
Ovanstående program uppvisar odefinierat beteende eftersom det innehåller två definitioner av klassen ::Foo
, som har extern länk, i olika översättningsenheter, men de två definitionerna är inte identiska. Till skillnad från omdefinition av en klass inom samma översättningsenhet krävs inte detta problem för att diagnostiseras av kompilatorn.
Felaktig parning av minnesallokering och omlokalisering
Ett objekt kan endast delas om genom att delete
om det tilldelades av new
och inte är ett array. Om argumentet för att delete
inte returnerades av new
eller är en matris, är beteendet odefinierat.
Ett objekt kan bara delas om genom att delete[]
om det tilldelades av new
och är en matris. Om argumentet för att delete[]
inte returnerades av new
eller inte är en matris, definieras beteendet.
Om argumentet att free
inte returnerades av malloc
, är beteendet odefinierat.
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
Sådana problem kan undvikas genom att helt undvika malloc
och free
i C ++ -program, föredra standardbibliotekets smarta pekare framför rå new
och delete
, och föredra std::vector
och std::string
framför rå new
och delete[]
.
Åtkomst till ett objekt som fel typ
I de flesta fall är det olagligt att komma åt ett objekt av en typ som om det var en annan typ (bortsett från cv-kval). Exempel:
float x = 42;
int y = reinterpret_cast<int&>(x);
Resultatet är odefinierat beteende.
Det finns några undantag från denna strikta aliaseringsregel :
- Ett objekt av klasstyp kan nås som om det vore av en typ som är en basklass av den faktiska klasstypen.
- Du kan få åtkomst till valfri typ som en
char
ellerunsigned char
, men det omvända är inte sant: en char array kan inte nås som om det var en godtycklig typ. - En signerad heltalstyp kan nås som motsvarande osignerad typ och vice versa .
En relaterad regel är att om en icke-statisk medlemsfunktion anropas till ett objekt som faktiskt inte har samma typ som den definierande klassen för funktionen, eller en härledd klass, inträffar odefinierat beteende. Detta gäller även om funktionen inte har åtkomst till objektet.
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
Flytande överflöd
Om en aritmetisk operation som ger en flytande punkttyp ger ett värde som inte ligger inom intervallet för representabla värden för resultattypen, är beteendet odefinierat enligt C ++ -standarden, men kan definieras av andra standarder som maskinen kan uppfylla, såsom IEEE 754.
float x = 1.0;
for (int i = 0; i < 10000; i++) {
x *= 10.0; // will probably overflow eventually; undefined behavior
}
Ringa (rena) virtuella medlemmar från konstruktör eller förstörare
Standarden (10.4) anger:
Medlemsfunktioner kan kallas från en konstruktör (eller destruktor) av en abstrakt klass; effekten av att ringa ett virtuellt samtal (10.3) till en ren virtuell funktion direkt eller indirekt för det objekt som skapas (eller förstörs) från en sådan konstruktör (eller destruktor) är odefinierad.
Mer generellt föreslår vissa C ++ myndigheter, t.ex. Scott Meyers, att de aldrig kallar virtuella funktioner (även icke-rena) från konstruktörer och dstruktorer.
Tänk på följande exempel, ändrat från ovanstående länk:
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 */ }
};
Anta att vi skapar ett sell_transaction
objekt:
sell_transaction s;
Detta kräver implicit konstruktören av sell_transaction
, som först anropar konstruktör av transaction
. När konstruktören för transaction
kallas men är objektet ännu inte av typen sell_transaction
, utan snarare endast av typen transaction
.
Följaktligen kommer samtalet i transaction::transaction()
till log_it
inte att göra det som kan tyckas vara den intuitiva saken - nämligen call sell_transaction::log_it
.
Om
log_it
är rent virtuellt, som i det här exemplet, är beteendet odefinierat.Om
log_it
är icke-rent virtuellt kommertransaction::log_it
att kallas.
Radera ett härledt objekt via en pekare till en basklass som inte har en virtuell förstörare.
class base { };
class derived: public base { };
int main() {
base* p = new derived();
delete p; // The is undefined behavior!
}
I avsnitt [expr.delete] §5.3.5 / 3 säger standarden att om delete
kallas på ett objekt vars statiska typ inte har en virtual
destruktor:
Om den statiska typen av objektet som ska raderas skiljer sig från dess dynamiska typ, ska den statiska typen vara en basklass för den dynamiska typen av objektet som ska raderas och den statiska typen ska ha en virtuell förstörare eller beteendet är odefinierat.
Detta är fallet oavsett frågan om den härledda klassen har lagt till några dataledamöter till basklassen.
Åtkomst till en dinglande referens
Det är olagligt att få tillgång till en referens till ett objekt som har gått ut ur tillämpningsområdet eller på annat sätt förstörts. En sådan referens sägs vara dinglande eftersom den inte längre hänvisar till ett giltigt objekt.
#include <iostream>
int& getX() {
int x = 42;
return x;
}
int main() {
int& r = getX();
std::cout << r << "\n";
}
I det här exemplet den lokala variabeln x
går ur ramen när getX
avkastning. (Observera att livstidsförlängningen inte kan förlänga livslängden för en lokal variabel förbi räckvidden för blocket i vilket det är definierat.) r
är därför en dinglande referens. Detta program har odefinierat beteende, även om det kan tyckas fungera och skriva ut 42
i vissa fall.
Utöka namnet "std" eller "posix"
Standarden (17.6.4.2.1 / 1) förbjuder i allmänhet att utvidga std
namnområdet:
Uppträdandet för ett C ++ -program definieras om det lägger till deklarationer eller definitioner till namnområdet std eller till ett namnutrymme inom namnområdet std om inte annat anges.
Detsamma gäller för posix
(17.6.4.2.2 / 1):
Uppträdandet för ett C ++ -program definieras om det lägger till deklarationer eller definitioner till namnområdet posix eller till ett namnområde i namnområdet posix om inte annat anges.
Tänk på följande:
#include <algorithm>
namespace std
{
int foo(){}
}
Ingenting i standarden förbjuder algorithm
(eller en av de rubriker som den innehåller) som definierar samma definition, och därför skulle denna kod bryta mot One Definition-regeln .
Så i allmänhet är detta förbjudet. Det finns dock vissa undantag . Kanske mest användbart är det tillåtet att lägga till specialiseringar för användardefinierade typer. Så antar till exempel att din kod har
class foo
{
// Stuff
};
Då är följande bra
namespace std
{
template<>
struct hash<foo>
{
public:
size_t operator()(const foo &f) const;
};
}
Överflöde under konvertering till eller från flytande punkttyp
Om under konverteringen av:
- en heltalstyp till en flytande punkttyp,
- en flytande punkttyp till en heltalstyp, eller
- en flytande punkttyp till en kortare flytande punkttyp,
källvärdet ligger utanför värdet som kan representeras i destinationstypen, resultatet är odefinierat beteende. Exempel:
double x = 1e100;
int y = x; // int probably cannot hold numbers that large, so this is UB
Ogiltig bas-till-härledd statisk gjutning
Om static_cast
används för att konvertera en pekare (resp. Referens) till basklass till en pekare (resp. Referens) till härledd klass, men operanden pekar inte (resp. Referens) till ett objekt av den härledda klasstypen, beteendet är odefinierad. Se Base till härledd konvertering .
Funktionssamtal genom felinställd funktionspekartyp
För att ringa en funktion via en funktionspekare måste funktionen pekarens typ exakt matcha funktionens typ. Annars är beteendet odefinierat. Exempel:
int f();
void (*p)() = reinterpret_cast<void(*)()>(f);
p(); // undefined
Ändra ett const-objekt
Varje försök att modifiera en const
objekt resulterar i odefinierade beteende. Detta gäller const
variabler, medlemmar av const
objekt och klassmedlemmar som deklarerats const
. (Men en mutable
medlem i ett const
objekt är inte const
.)
Ett sådant försök kan göras genom const_cast
:
const int x = 123;
const_cast<int&>(x) = 456;
std::cout << x << '\n';
En kompilator kommer vanligtvis att ange värdet på ett const int
objekt, så det är möjligt att den här koden sammanställer och skriver ut 123
. Kompilatorer kan också placera const
värden i skrivskyddat minne, så det kan uppstå ett segmenteringsfel. I alla fall är beteendet odefinierat och programmet kan göra vad som helst.
Följande program döljer ett mycket mer subtilt fel:
#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';
}
I denna kod skapar getFoo
en singleton av typen const Foo
och dess medlem m_x
initialiseras till 123
. Sedan do_evil
och värdet på foo.m_x
ändras tydligen till 456. Vad gick fel?
Trots sitt namn gör do_evil
inget särskilt ont; allt det gör är att ringa en setter genom en Foo*
. Men den pekaren pekar på ett const Foo
objekt trots att const_cast
inte användes. Denna pekare erhölls genom Foo
konstruktör. Ett const
objekt blir inte const
förrän dess initialisering är klar, så this
har typen Foo*
, inte const Foo*
, inom konstruktören.
Därför inträffar odefinierat beteende även om det inte finns några uppenbara farliga konstruktioner i detta program.
Tillgång till obefintlig medlem via pekaren till medlemmen
När åtkomst till ett icke-statiskt medlem av ett objekt via en pekare till medlem, om objektet inte faktiskt innehåller det medlem som är markerat av pekaren, är beteendet odefinierat. (En sådan pekare till medlem kan erhållas 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
Ogiltig härledd-till-baskonvertering för pekare till medlemmar
När static_cast
används för att konvertera TD::*
till TB::*
måste medlemmen som pekas till tillhöra en klass som är en basklass eller härledd klass B
Annars är beteendet odefinierat. Se härledd för att basera konvertering för pekare till medlemmar
Ogiltig pekar aritmetik
Följande användningar av pekaren aritmetiska orsakar odefinierat beteende:
Tillsats eller subtraktion av ett heltal, om resultatet inte tillhör samma arrayobjekt som pekaren operand. (Här anses elementet en förbi slutet fortfarande tillhöra matrisen.)
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]
Subtraktion av två pekare om de inte båda tillhör samma arrayobjekt. (Återigen anses elementet som går förbi slutet tillhöra matrisen.) Undantaget är att två nollpekare kan subtraheras och ger 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
Subtraktion av två pekare om resultatet överflödar
std::ptrdiff_t
.Alla pekare aritmetiska där antingen operandens pointee-typ inte stämmer överens med den dynamiska typen av objektet som pekas på (ignorerar cv-kvalificering). Enligt standarden, "[i synnerhet], kan inte en pekare till en basklass användas för pekare aritmetik när matrisen innehåller objekt av en härledd klasstyp."
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
Skiftning med ett ogiltigt antal positioner
För den inbyggda skiftoperatören måste höger operand vara icke-negativ och strikt mindre än bitbredden för den främjade vänstra operanden. Annars är beteendet odefinierat.
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
Återgå från en [[noreturn]] -funktion
Exempel från standarden, [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";
}
Förstör ett objekt som redan har förstörts
I det här exemplet anropas en förstörare uttryckligen för ett objekt som senare automatiskt förstörs.
struct S {
~S() { std::cout << "destroying S\n"; }
};
int main() {
S s;
s.~S();
} // UB: s destroyed a second time here
Ett liknande problem inträffar när en std::unique_ptr<T>
görs att peka på en T
med automatisk eller statisk lagringsvaraktighet.
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
Ett annat sätt att förstöra ett objekt två gånger är genom att ha två shared_ptr
er båda hantera objektet utan att dela ägande med varandra.
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);
Oändlig mallrekursion
Exempel från standarden, [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 ...
};