Ricerca…


introduzione

Cos'è il comportamento non definito (UB)? Secondo lo standard ISO C ++ (§1.3.24, N4296), è "un comportamento per il quale questo Standard internazionale non impone alcun requisito".

Ciò significa che quando un programma incontra UB, è permesso di fare tutto ciò che vuole. Questo spesso significa un crash, ma potrebbe semplicemente non fare nulla, far volare via demoni dal demone , o persino sembrare che funzioni correttamente!

Inutile dire che dovresti evitare di scrivere codice che invoca UB.

Osservazioni

Se un programma contiene un comportamento non definito, lo standard C ++ non pone alcun vincolo al suo comportamento.

  • Potrebbe sembrare che funzioni come previsto dallo sviluppatore, ma potrebbe anche bloccarsi o produrre risultati strani.
  • Il comportamento può variare tra le esecuzioni dello stesso programma.
  • Qualsiasi parte del programma potrebbe funzionare male, incluse le linee che precedono la linea che contiene un comportamento non definito.
  • L'implementazione non è richiesta per documentare il risultato di un comportamento non definito.

Un'implementazione può documentare il risultato di un'operazione che produce un comportamento non definito secondo lo standard, ma un programma che dipende da tale comportamento documentato non è portabile.

Perché esiste un comportamento non definito

Intuitivamente, il comportamento non definito è considerato un aspetto negativo in quanto tali errori non possono essere gestiti in modo gentile attraverso, ad esempio, gestori di eccezioni.

Ma lasciare un certo comportamento indefinito è in realtà parte integrante della promessa del C ++ "non si paga per ciò che non si usa". Un comportamento indefinito consente al compilatore di assumere che lo sviluppatore sappia cosa sta facendo e di non introdurre il codice per verificare gli errori evidenziati negli esempi precedenti.

Trovare ed evitare comportamenti indefiniti

Alcuni strumenti possono essere utilizzati per scoprire comportamenti non definiti durante lo sviluppo:

  • La maggior parte dei compilatori dispone di flag di avviso per avvisare di alcuni casi di comportamento non definito al momento della compilazione.
  • Le versioni più recenti di gcc e clang includono un cosiddetto indicatore "Undefined Behavior Sanitizer" ( -fsanitize=undefined ) che verificherà il comportamento indefinito in fase di esecuzione, a un costo prestazionale.
  • lint strumenti simili a sfilacciamenti possono eseguire analisi del comportamento indefinite più approfondite.

Comportamento indefinito, non specificato e definito dall'implementazione

Dalla norma C ++ 14 (ISO / IEC 14882: 2014) sezione 1.9 (Esecuzione del programma):

  1. Le descrizioni semantiche di questo standard internazionale definiscono una macchina astratta non parametrica parametrizzata. [TAGLIO]

  2. Alcuni aspetti e operazioni della macchina astratta sono descritti in questo Standard Internazionale come definito dall'implementazione (ad esempio, sizeof(int) ). Questi costituiscono i parametri della macchina astratta . Ogni implementazione deve includere la documentazione che descrive le sue caratteristiche e il comportamento in questi aspetti. [TAGLIO]

  3. Alcuni altri aspetti e operazioni della macchina astratta sono descritti in questo Standard Internazionale come non specificati (ad esempio, la valutazione delle espressioni in un nuovo inizializzatore se la funzione di allocazione non riesce ad allocare memoria). Ove possibile, questo standard internazionale definisce un insieme di comportamenti consentiti. Questi definiscono gli aspetti non deterministici della macchina astratta. Un'istanza della macchina astratta può quindi avere più di una possibile esecuzione per un dato programma e un dato input.

  4. Alcune altre operazioni sono descritte in questo standard internazionale come non definite (o esempio, l'effetto del tentativo di modificare un oggetto const ). [ Nota : questo Standard Internazionale non impone alcun requisito sul comportamento dei programmi che contengono comportamenti non definiti. - nota finale ]

Leggere o scrivere attraverso un puntatore nullo

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

Questo comportamento non è definito , poiché un puntatore nullo non punta a nessun oggetto valido, quindi non vi è alcun oggetto su *ptr in cui scrivere.

Sebbene questo spesso causi un errore di segmentazione, non è definito e può succedere di tutto.

Nessuna dichiarazione di reso per una funzione con un tipo di reso non vuoto

Omettere l'istruzione return in una funzione che ha un tipo restituito che non è void è un comportamento indefinito .

int function() {  
    // Missing return statement
} 

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

I compilatori più moderni emettono un avviso in fase di compilazione per questo tipo di comportamento indefinito.


Nota: main è l'unica eccezione alla regola. Se main non ha un'istruzione return , il compilatore inserisce automaticamente return 0; per te, quindi può essere tranquillamente tralasciato.

Modifica di una stringa letterale

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

"hello world" è una stringa letterale, quindi modificarla dà un comportamento indefinito.

L'inizializzazione di str nell'esempio precedente è stata formalmente deprecata (programmata per la rimozione da una versione futura dello standard) in C ++ 03. Un certo numero di compilatori prima del 2003 potrebbe emettere un avviso a riguardo (ad esempio una conversione sospetta). Dopo il 2003, i compilatori in genere mettono in guardia su una conversione deprecata.

C ++ 11

L'esempio sopra è illegale e risulta in una diagnostica del compilatore, in C ++ 11 e versioni successive. Un esempio simile può essere costruito per mostrare un comportamento non definito consentendo esplicitamente la conversione del tipo, come ad esempio:

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

Accedere a un indice fuori dai limiti

È un comportamento indefinito accedere a un indice che è fuori limite per un array (o contenitore di libreria standard per quello scopo, poiché sono tutti implementati utilizzando un array raw ):

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

È consentito avere un puntatore che punta alla fine dell'array (in questo caso array + 5 ), non è possibile dereferenziarlo, poiché non è un elemento valido.

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

In generale, non ti è permesso creare un puntatore fuori limite. Un puntatore deve puntare a un elemento all'interno dell'array o uno dopo la fine.

Divisione intera per zero

int x = 5 / 0;    // Undefined behavior

La divisione per 0 è matematicamente indefinita e, in quanto tale, ha senso che si tratti di un comportamento non definito.

Però:

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

La maggior parte dell'attuazione implementa IEEE-754, che definisce la divisione in virgola mobile per zero per restituire NaN (se il numeratore è 0.0f ), infinity (se il numeratore è positivo) o -infinity (se il numeratore è negativo).

Overflow intero firmato

int x = INT_MAX + 1;

// x can be anything -> Undefined behavior

Se durante la valutazione di un'espressione, il risultato non è definito matematicamente o non rientra nell'intervallo di valori rappresentabili per il suo tipo, il comportamento non è definito.

(C ++ 11 Standard paragrafo 5/4)

Questo è uno dei più cattivi, dato che di solito produce un comportamento riproducibile e non-schiantarsi così gli sviluppatori potrebbero essere tentati di fare molto affidamento sul comportamento osservato.


D'altro canto:

unsigned int x = UINT_MAX + 1;

// x is 0

è ben definito poiché:

Gli interi senza segno, dichiarati senza segno, obbediscono alle leggi dell'aritmetico modulo 2^n dove n è il numero di bit nella rappresentazione del valore di quella particolare dimensione del numero intero.

(C ++ 11 paragrafo standard 3.9.1 / 4)

A volte i compilatori possono sfruttare un comportamento indefinito e ottimizzare

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

Qui dal momento che un overflow di interi con segno non è definito, il compilatore è libero di presumere che non possa mai accadere e quindi può ottimizzare il blocco "se"

Utilizzando una variabile locale non inizializzata

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

Ciò si traduce in un comportamento indefinito , perché a non è inizializzato.

È spesso, erroneamente, affermato che ciò è dovuto al fatto che il valore è "indeterminato" o "qualsiasi valore si trovasse in quella posizione di memoria precedente". Tuttavia, è l'atto di accedere al valore di a nell'esempio precedente che fornisce un comportamento non definito. In pratica, stampare un "valore spazzatura" è un sintomo comune in questo caso, ma questa è solo una possibile forma di comportamento non definito.

Sebbene sia molto improbabile nella pratica (poiché dipende dal supporto hardware specifico), il compilatore potrebbe ugualmente elettrocurare il programmatore durante la compilazione del codice di esempio sopra. Con un tale compilatore e supporto hardware, una simile risposta a comportamenti non definiti aumenterebbe notevolmente la comprensione media del programmatore (vivente) del vero significato di comportamento non definito - il che è che lo standard non pone alcun vincolo sul comportamento risultante.

C ++ 14

L'utilizzo di un valore indeterminato di tipo unsigned char non produce un comportamento non definito se il valore viene utilizzato come:

  • il secondo o terzo operando dell'operatore condizionale ternario;
  • l'operando corretto dell'operatore virgola incorporato;
  • l'operando di una conversione in unsigned char ;
  • l'operando corretto dell'operatore di assegnazione, se l'operando di sinistra è anch'esso di tipo unsigned char ;
  • l'inizializzatore per un oggetto unsigned char ;

o se il valore è scartato. In questi casi, il valore indeterminato si propaga semplicemente al risultato dell'espressione, se applicabile.

Si noti che una variabile static è sempre inizializzata a zero (se possibile):

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

Definizioni multiple non identiche (la regola One Definition)

Se una classe, enum, funzione inline, modello o membro di un modello ha un collegamento esterno ed è definita in più unità di traduzione, tutte le definizioni devono essere identiche o il comportamento non è definito in base alla 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
}

Il programma sopra esposto presenta un comportamento indefinito perché contiene due definizioni della classe ::Foo , che ha un collegamento esterno, in diverse unità di traduzione, ma le due definizioni non sono identiche. A differenza della ridefinizione di una classe all'interno della stessa unità di traduzione, non è richiesto che questo problema venga diagnosticato dal compilatore.

Associazione errata di allocazione e deallocazione di memoria

Un oggetto può essere deallocato solo da delete se è stato assegnato da un new e non è un array. Se l'argomento da delete non è stato restituito da new o è un array, il comportamento non è definito.

Un oggetto può essere deallocato solo da delete[] se è stato assegnato da new ed è un array. Se l'argomento per delete[] non è stato restituito da new o non è un array, il comportamento non è definito.

Se l'argomento free non è stato restituito da malloc , il comportamento non è definito.

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

Tali problemi possono essere evitati evitando completamente malloc e free nei programmi C ++, preferendo i puntatori intelligenti della libreria standard su raw new e delete , e preferendo std::vector e std::string su raw new e delete[] .

Accedere a un oggetto come il tipo sbagliato

Nella maggior parte dei casi, è illegale accedere a un oggetto di un tipo come se fosse di un tipo diverso (ignorando i qualificatori di cv). Esempio:

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

Il risultato è un comportamento indefinito.

Ci sono alcune eccezioni a questa rigida regola di aliasing :

  • È possibile accedere a un oggetto di tipo classe come se fosse di un tipo che è una classe base del tipo effettivo di classe.
  • Qualsiasi tipo è accessibile come char o unsigned char , ma il contrario non è vero: non è possibile accedere a un array di caratteri come se fosse un tipo arbitrario.
  • Un tipo intero con segno è accessibile come corrispondente senza segno e viceversa .

Una regola correlata è che se una funzione membro non statica viene chiamata su un oggetto che in realtà non ha lo stesso tipo della classe di definizione della funzione o di una classe derivata, si verifica un comportamento non definito. Questo è vero anche se la funzione non accede all'oggetto.

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

Overflow a virgola mobile

Se un'operazione aritmetica che produce un tipo a virgola mobile produce un valore che non è compreso nell'intervallo di valori rappresentabili del tipo di risultato, il comportamento non è definito secondo lo standard C ++, ma può essere definito da altri standard a cui la macchina potrebbe conformarsi, come IEEE 754.

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

Chiamare membri (puri) virtuali da Costruttore o Distruttore

Lo standard (10.4) afferma:

Le funzioni membro possono essere chiamate da un costruttore (o distruttore) di una classe astratta; l'effetto di effettuare una chiamata virtuale (10.3) a una pura funzione virtuale direttamente o indirettamente per l'oggetto creato (o distrutto) da tale costruttore (o distruttore) non è definito.

Più in generale, alcune autorità del C ++, ad esempio Scott Meyers, suggeriscono di non chiamare mai funzioni virtuali (anche non pure) da costruttori e dstructors.

Considera il seguente esempio, modificato dal link precedente:

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

Supponiamo di creare un oggetto sell_transaction :

sell_transaction s;

Questo chiama implicitamente il costruttore di sell_transaction , che prima chiama il costruttore della transaction . Tuttavia, quando viene chiamato il costruttore della transaction , l'oggetto non è ancora del tipo sell_transaction , ma solo della transaction tipo.

Di conseguenza, la chiamata in transaction::transaction() a log_it , non farà ciò che potrebbe sembrare la cosa intuitiva, ovvero call sell_transaction::log_it .

  • Se log_it è puro virtuale, come in questo esempio, il comportamento non è definito.

  • Se log_it non è puro virtuale, verrà chiamato transaction::log_it .

Eliminazione di un oggetto derivato tramite un puntatore a una classe base che non ha un distruttore virtuale.

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

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

Nella sezione [expr.delete] §5.3.5 / 3 lo standard dice che se viene chiamato delete su un oggetto il cui tipo statico non ha un distruttore virtual :

se il tipo statico dell'oggetto da eliminare è diverso dal suo tipo dinamico, il tipo statico deve essere una classe base del tipo dinamico dell'oggetto da eliminare e il tipo statico deve avere un distruttore virtuale o il comportamento non è definito.

Questo è il caso indipendentemente dalla domanda se la classe derivata ha aggiunto membri di dati alla classe base.

Accedere a un riferimento ciondolante

È illegale accedere a un riferimento a un oggetto che è andato fuori portata o è stato altrimenti distrutto. Si dice che tale riferimento penzoli poiché non si riferisce più ad un oggetto valido.

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

In questo esempio, la variabile locale x diventa fuori campo quando restituisce getX . (Si noti che l' estensione a vita non può estendere la durata di una variabile locale oltre l'ambito del blocco in cui è definita.) Pertanto r è un riferimento ciondolante. Questo programma ha un comportamento indefinito, anche se può sembrare che funzioni e stampi 42 in alcuni casi.

Estendere lo spazio dei nomi `std` o` posix`

Lo standard (17.6.4.2.1 / 1) generalmente vieta di estendere lo spazio dei nomi std :

Il comportamento di un programma C ++ non è definito se aggiunge dichiarazioni o definizioni allo spazio dei nomi std o ad uno spazio dei nomi all'interno dello std del namespace, se non diversamente specificato.

Lo stesso vale per posix (17.6.4.2.2 / 1):

Il comportamento di un programma C ++ non è definito se aggiunge dichiarazioni o definizioni allo spazio dei nomi posix o a uno spazio dei nomi all'interno di posizione spazio dei nomi, se non diversamente specificato.

Considera quanto segue:

#include <algorithm>

namespace std
{
    int foo(){}
}

Niente nello standard proibisce l' algorithm (o una delle intestazioni che include) che definisce la stessa definizione, e quindi questo codice violerebbe la regola di una definizione .

Quindi, in generale, questo è proibito. Tuttavia, sono consentite eccezioni specifiche . Forse la cosa più utile è che è possibile aggiungere specializzazioni per i tipi definiti dall'utente. Quindi, per esempio, supponiamo che il tuo codice abbia

class foo
{
    // Stuff
};

Quindi quanto segue va bene

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

Overflow durante la conversione in o dal tipo a virgola mobile

Se, durante la conversione di:

  • un tipo intero in un tipo a virgola mobile,
  • un tipo a virgola mobile su un tipo intero o
  • un tipo a virgola mobile su un tipo a virgola mobile più corto,

il valore di origine è al di fuori dell'intervallo di valori che può essere rappresentato nel tipo di destinazione, il risultato è un comportamento non definito. Esempio:

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

Trasmissione statica da base a derivata non valida

Se static_cast viene utilizzato per convertire un puntatore (riferimento) in una classe derivata in un puntatore (riferimento) alla classe derivata, ma l'operando non punta (si riferisce) a un oggetto del tipo di classe derivata, il comportamento è indefinito. Vedi la conversione da Base a derivata .

Chiamata di funzione tramite il tipo di puntatore a funzione non corrispondente

Per chiamare una funzione attraverso un puntatore a funzione, il tipo di puntatore della funzione deve corrispondere esattamente al tipo di funzione. Altrimenti, il comportamento non è definito. Esempio:

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

Modifica di un oggetto const

Qualsiasi tentativo di modificare un oggetto const risulta in un comportamento non definito. Questo si applica alle variabili const , ai membri degli oggetti const e ai membri della classe dichiarati const . (Tuttavia, un mutable membro di un const oggetto non è const .)

Un tale tentativo può essere fatto tramite const_cast :

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

Un compilatore di solito inline il valore di un oggetto const int , quindi è possibile che questo codice compili e stampi 123 . I compilatori possono anche posizionare i valori degli oggetti const nella memoria di sola lettura, pertanto potrebbe verificarsi un errore di segmentazione. In ogni caso, il comportamento non è definito e il programma potrebbe fare qualsiasi cosa.

Il seguente programma nasconde un errore molto più sottile:

#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 questo codice, getFoo crea un singleton di tipo const Foo e il suo membro m_x è inizializzato a 123 . Quindi viene chiamato do_evil e il valore di foo.m_x apparentemente viene modificato in 456. Che cosa è andato storto?

Nonostante il suo nome, do_evil non fa nulla di particolarmente malvagio; tutto ciò che fa è chiamare un setter attraverso un Foo* . Ma quel puntatore punta a un oggetto const Foo anche se const_cast non è stato usato. Questo puntatore è stato ottenuto tramite il costruttore di Foo . Un oggetto const non diventa const finché la sua inizializzazione non è completa, quindi this ha tipo Foo* , non const Foo* , all'interno del costruttore.

Pertanto, si verifica un comportamento indefinito anche se non ci sono costrutti chiaramente pericolosi in questo programma.

Accesso a membri inesistenti tramite puntatore al membro

Quando si accede a un membro non statico di un oggetto tramite un puntatore al membro, se l'oggetto non contiene effettivamente il membro indicato dal puntatore, il comportamento non è definito. (Tale puntatore al membro può essere ottenuto tramite 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

Conversione da origine a base non valida per i puntatori ai membri

Quando static_cast viene utilizzato per convertire TD::* in TB::* , il membro a cui fa riferimento deve appartenere a una classe che è una classe base o una classe derivata di B Altrimenti il ​​comportamento non è definito. Vedi Conversione da Derivata a base per i puntatori ai membri

Aritmetica del puntatore non valida

I seguenti usi dell'aritmetica del puntatore causano un comportamento non definito:

  • Aggiunta o sottrazione di un intero, se il risultato non appartiene allo stesso oggetto matrice dell'operando puntatore. (Qui, si considera che l'elemento uno oltre la fine appartenga ancora alla matrice.)

    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]
    
  • Sottrazione di due puntatori se non appartengono allo stesso oggetto matrice. (Di nuovo, l'elemento uno oltre la fine è considerato appartenere all'array.) L'eccezione è che due puntatori nulli possono essere sottratti, ottenendo 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
    
  • Sottrazione di due puntatori se il risultato supera lo std::ptrdiff_t .

  • Qualsiasi aritmetica puntatore in cui il tipo di punta dell'operando non corrisponde al tipo dinamico dell'oggetto puntato (ignorando la qualifica cv). Secondo lo standard, "[in] in particolare, un puntatore a una classe base non può essere utilizzato per l'aritmetica del puntatore quando l'array contiene oggetti di un tipo di classe derivata."

    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
    

Spostamento di un numero non valido di posizioni

Per l'operatore di spostamento incorporato, l'operando di destra deve essere non negativo e strettamente inferiore alla larghezza di bit dell'operando sinistro promosso. Altrimenti, il comportamento non è definito.

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

Ritorno da una funzione [[Noreturn]]

C ++ 11

Esempio dallo standard, [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";
}

Distruggere un oggetto che è già stato distrutto

In questo esempio, un distruttore viene invocato esplicitamente per un oggetto che verrà successivamente distrutto automaticamente.

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

Un problema simile si verifica quando uno std::unique_ptr<T> viene creato per puntare a una T con durata di archiviazione automatica o statica.

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

Un altro modo per distruggere un oggetto due volte consiste nel fatto che due shared_ptr s gestiscono entrambi l'oggetto senza condividere la proprietà l'uno con l'altro.

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

Ricorsione infinita del modello

Esempio dallo standard, [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
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow