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

  1. De semantiska beskrivningarna i denna internationella standard definierar en parametrerad nondeterministisk abstrakt maskin. [SKÄRA]

  2. 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]

  3. 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.

  4. 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

C ++ 11
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.

C ++ 11

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 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är n ä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.

C ++ 14

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 eller unsigned 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 kommer transaction::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

C ++ 11

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 ...
};


Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow