2012-10-07 24 views
12

Ho scritto this article e ho ricevuto alcuni commenti che mi hanno confuso.È vero che una dichiarazione unique_ptr, a differenza di una dichiarazione auto_ptr, è ben definita quando il suo tipo di modello è di tipo incompleto?

Si riduce sostanzialmente verso il basso per il mio aver visto T2 utilizzato solo come un parametro di template e erroneamente saltato alla conclusione che ho potuto quindi cogliere l'occasione della dichiarazione anticipata:

struct T2; 

struct T1 
{ 
    std::auto_ptr<T2> obj; 
}; 

Questo richiama UB se I don' t andare a definire T2 da qualche parte nello stesso TU, perché std::auto_ptr<T2> chiamate delete sul suo interno T2*, e calling delete on an pointer to an object of an incomplete type whose complete type has a non-trivial destructor is undefined:

[C++11: 5.3.5/5]: Se l'oggetto da eliminare ha un tipo di classe incompleto al punto di eliminazione e la classe completa ha un distruttore non banale o una funzione di deallocazione, il comportamento non è definito.

La toolchain GCC mi è capitato di utilizzare — v4.3.3 (Sourcery G ++ Lite 2009q1-203) — stato così gentile da farmi sapere con una nota:

nota: né il distruttore, né verrà chiamato l'operatore dell'operatore specifico della classe, anche se vengono dichiarati al momento della definizione della classe.

anche se sembra difficile ottenere questa diagnostica in altre versioni di GCC.

La mia lamentela era che sarebbe stato molto più facile da individuare un bug come questo se delete ing un puntatore a un'istanza di un tipo incompleto sono stati mal formate piuttosto che UB, ma che sembra un problema di intractible per un'implementazione da risolvere, quindi capisco perché è UB.

Ma poi mi viene detto che, se dovessi usare std::unique_ptr<T2> invece, questo sarebbe sicuro e conforme.

n3035 presumibilmente dice a 20.9.10.2:

Il parametro di modello T di unique_ptr può essere un tipo incompleto.

Tutto quello che posso trovare in C++ 11 corretta è:

[C++11: 20.7.1.1.1]:

/1 Il modello di classe default_delete serve come predefinita deleter (politica distruzione) per il modello di classe unique_ptr.

/2 Il parametro template T di default_delete può essere un tipo incompleto.

Ma, default_delete s' operator() richiede un tipo completo:

[C++11: 20.7.1.1.2/4]: Se T è un tipo incompleto, il programma è mal-formata.


Suppongo che la mia domanda è questa:

Sono i commentatori sul mio articolo corretto nel dire che un'unità di traduzione che consiste di solo il seguente codice è ben formata e ben definito? O hanno torto?

struct T2; 

struct T1 
{ 
    std::unique_ptr<T2> obj; 
}; 

se sono corrette, come è un compilatore prevede di implementare questo, dato che ci sono buone ragioni per questo che sono UB, almeno quando un std::auto_ptr viene utilizzato?

+0

Poiché la dicitura è "il programma è mal formato" (senza esplicito "e non è richiesta alcuna diagnostica") e non ad es. 'questo si traduce in un comportamento indefinito', questo * fa * significa che è necessaria una diagnostica (che può essere implementata come 'static_assert (sizeof (T)," ")'). Quindi sì, 'std :: unique_ptr ' è più sicuro di 'std :: auto_ptr '. (Si noti che ciò è dovuto all'uso del default 'std :: default_delete ' deleter.) –

+1

Volevo chiarire che 'il codice è ben formato e ben definito'/'c'è UB' aren ' gli unici due possibili risultati. (Potrebbe non essere quello che volevi trasmettere ma preferirei essere esplicito.) –

risposta

9

Secondo Herb Sutter in GOTW #100, unique_ptr soffre dello stesso problema auto_ptr rispetto ai tipi incompleti.

... anche se entrambi unique_ptr e shared_ptr possono essere istanziati con un tipo incompleto , distruttore di unique_ptr richiede un tipo completo in per invocare cancellare ...

Il suo suggerimento è quello di dichiarare la distruttore della classe contenente (ovvero T1) nel file di intestazione, quindi posizionare la sua definizione in un'unità di traduzione in cui T2 è un tipo completo.

// T1.h 
struct T2; 

struct T1 
{ 
    ~T1(); 
    std::unique_ptr<T2>; 
}; 

// T1.cpp 
#include "T2.h" 

T1::~T1() 
{ 
} 
+1

Solo 'std :: unique_ptr ' ha questa proprietà, non 'std :: unique_ptr' del tutto, e non lo chiamerei un problema . Se stai bene pagando i costi di cancellazione del tipo, puoi utilizzare ad es. un 'std :: unique_ptr >' e passa un deleter appropriato al momento della costruzione. –

+0

Un'alternativa è 'std :: unique_ptr ' (insieme con ad esempio un lambda senza acquisizione) in cui il costo è invece un riferimento indiretto. –

8

Il seguente esempio è un tentativo di dimostrare la differenza tra std::auto_ptr<T> e std::unique_ptr<T>. In primo luogo prendere in considerazione questo programma composto da 2 file di origine e 1 intestazione:

L'intestazione:

// test.h 

#ifndef TEST_H 
#define TEST_H 

#include <memory> 

template <class T> 
using smart_ptr = std::auto_ptr<T>; 

struct T2; 

struct T1 
{ 
    smart_ptr<T2> obj; 

    T1(T2* p); 
}; 

T2* 
source(); 

#endif // TEST_H 

prima fonte:

// test.cpp 

#include "test.h" 

int main() 
{ 
    T1 t1(source()); 
} 

seconda fonte:

// test2.cpp 

#include "test.h" 
#include <iostream> 


struct T2 
{ 
    ~T2() {std::cout << "~T2()\n";} 
}; 

T1::T1(T2* p) 
    : obj(p) 
{ 
} 

T2* 
source() 
{ 
    return new T2; 
} 

Questo programma dovrebbe compilare (può compilare con un avvertimento, ma dovrebbe compilare). Ma in fase di esecuzione dimostra un comportamento indefinito. E probabilmente non sarà in uscita:

~T2() 

che indica che distruttore T2 s' non è stato eseguito. Almeno non sul mio sistema.

Se cambio test.h a:

template <class T> 
using smart_ptr = std::unique_ptr<T>; 

Poi il compilatore è tenuto a emettere un diagnostico (un errore).

Cioè, quando si commette questo errore con auto_ptr si ottiene un errore di runtime. Quando si commette questo errore con unique_ptr si ottiene un errore di compilazione. E che è la differenza tra auto_ptr e unique_ptr.

Per correggere l'errore di compilazione, è necessario indicare ~T1() dopo che T2 è stato completato. In test2.cpp aggiungere dopo T2:

T1::~T1() = default; 

ora, dovrebbe compilare ed uscita:

~T2() 

è probabile che vuole dichiarare e membri contorno mossa così:

T1::T1(T1&&) = default; 
T1& T1::operator=(T1&&) = default; 

È potrebbe fare queste stesse correzioni con auto_ptr e sarebbe di nuovo corretto. Ma ancora una volta, la differenza tra auto_ptr e unique_ptr è che con il primo, non si scopre fino a tempo di esecuzione che si ha qualche debug da fare (modulo opzionale di avvertimenti che il compilatore può dare). Con quest'ultimo si è sicuri di scoprirlo in fase di compilazione.