2012-10-07 16 views
12

Ich schrieb this article und bekam einige Kommentare, die mich verwirrten.Stimmt es, dass eine unique_ptr-Deklaration im Gegensatz zu einer auto_ptr-Deklaration gut definiert ist, wenn der Vorlagentyp nicht vollständig ist?

Es läuft darauf hinaus grundsätzlich bis zu meinem T2 nur als Template-Parameter verwendet, gesehen zu haben und fälschlicherweise sprang zu dem Schluss, dass ich daher die Gelegenheit, nach vorne Erklärung nehmen könnte:

struct T2; 

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

Das ruft UB, wenn ich don‘ t gehen auf T2 irgendwo in der gleichen TU zu definieren, weil std::auto_ptr<T2> Anrufe delete auf seiner internen T2* und 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]: Wenn Das zu löschende Objekt hat zum Löschzeitpunkt einen unvollständigen Klassentyp und die vollständige Klasse hat einen nicht-trivialen Destruktor oder eine Deallokationsfunktion, das Verhalten ist nicht definiert.

Die GCC-Toolchain Ich war zufällig — v4.3.3 (Sourcery G ++ Lite 2009q1-203) — war nett genug zu sein mit mir mit einer Notiz wissen zu lassen:

Anmerkung: weder der Destruktor noch Der klassenspezifische Operator delete wird aufgerufen, auch wenn sie deklariert werden, wenn die Klasse definiert ist.

obwohl es schwierig scheint, diese Diagnose in anderen GCC-Versionen zu erhalten.

Mein Problem war, dass es viel einfacher wäre, einen Fehler wie diese, wenn delete ing einen Zeiger auf eine Instanz eines unvollständigen Typ schlecht gebildet wurden eher als UB zu erkennen, aber das scheint wie ein effizient lösbar Problem für eine Implementierung zu lösen, also verstehe ich, warum es UB ist.

Aber dann wurde mir gesagt, wenn ich stattdessen std::unique_ptr<T2> verwenden würde, wäre dies sicher und konform.

n3035 sagt angeblich bei 20.9.10.2:

Der Template-Parameter T von unique_ptr kann ein unvollständiger Typ sein.

Alle finde ich in C++ 11 richtig ist:

[C++11: 20.7.1.1.1]:

/1 Die Klassenvorlage default_delete als die (Zerstörung Politik) Standard deleter dient der Klassenvorlage unique_ptr.

/2 Der Vorlagenparameter T von default_delete ist möglicherweise ein unvollständiger Typ.

Aber default_delete ‚s operator() erfordert eine komplette Art:

[C++11: 20.7.1.1.2/4]: Wenn T ein unvollständiger Typ ist, wird das Programm schlecht ausgebildet.


Ich nehme meine Frage ist:

Sind die commenters auf meinem Artikel richtig zu sagen, dass eine Einheit Übersetzung nur den folgenden Code, der aus wohlgeformt ist und gut definiert? Oder sind sie falsch?

struct T2; 

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

Wenn sie richtig sind, wie ein Compiler dies, da zu implementieren erwartet, dass es gute Gründe dafür UB zu sein, zumindest wenn ein std::auto_ptr verwendet wird?

+0

Da die Formulierung "das Programm ist schlecht gebildet" (ohne explizite "und keine Diagnose erforderlich ist") und nicht z. 'Dies führt zu undefiniertem Verhalten', dies * bedeutet, dass eine Diagnose erforderlich ist (die als 'static_assert (sizeof (T)," ")' implementiert werden kann). Also ja, 'std :: unique_ptr ' ist sicherer als 'std :: auto_ptr '. (Bitte beachten Sie, dass dies auf die Verwendung des Standard-Löschers "std :: default_delete " zurückzuführen ist.) –

+1

Ich wollte klarstellen, dass "der Code wohlgeformt und wohldefiniert ist"/"es gibt UB" aren ' t die einzigen beiden möglichen Ergebnisse. (Es mag nicht das sein, was du vermitteln wolltest, aber ich möchte lieber explizit sein.) –

Antwort

9

Laut Herb Sutter in GOTW #100, unique_ptr leidet unter dem gleichen Problem wie auto_ptr in Bezug auf unvollständige Typen.

... obwohl beide unique_ptr und shared_ptr kann mit einem unvollständigen Typ, unique_ptr Destruktor erfordert eine komplette Art in Reihenfolge löschen ... ist

Sein Vorschlag aufzurufen instanziert werden die erklären Destruktor Ihrer enthaltenden Klasse (zB T1) in der Header-Datei, dann legen Sie die Definition in eine Übersetzungseinheit, in der T2 ein vollständiger Typ ist.

// T1.h 
struct T2; 

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

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

T1::~T1() 
{ 
} 
+1

Nur 'std :: unique_ptr ' hat diese Eigenschaft, nicht 'std :: unique_ptr' insgesamt, und ich würde es kein Problem nennen . Wenn Sie in der Lage sind, die Kosten für die Art der Löschung zu bezahlen, können Sie z. ein 'std :: unique_ptr >' und übergeben Sie einen geeigneten Deleter zur Konstruktionszeit. –

+0

Eine Alternative ist 'std :: unique_ptr ' (zusammen mit beispielsweise einem Capture-less Lambda), wobei die Kosten stattdessen eine Indirektion sind. –

8

Das folgende Beispiel ist ein Versuch, die Differenz zwischen std::auto_ptr<T> und std::unique_ptr<T> zu demonstrieren. Zuerst dieses Programm betrachtet, bestehend aus zwei Quelldateien und 1-Header:

Der Header:

// 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 

erste Quelle:

// test.cpp 

#include "test.h" 

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

Zweite Quelle:

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

Dieses Programm sollte kompilieren (es kann mit einer Warnung kompilieren, aber es sollte kompilieren). Aber zur Laufzeit zeigt es undefiniertes Verhalten. Und es wird wahrscheinlich nicht ausgegeben:

~T2() 

, die anzeigt, dass T2 ‚s destructor nicht ausgeführt wurde. Zumindest nicht auf meinem System.

Wenn ich ändern test.h zu:

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

Dann wird der Compiler-Ausgang erforderlich ist, eine Diagnose (einen Fehler).

Das heißt, wenn Sie diesen Fehler mit auto_ptr machen, erhalten Sie einen Laufzeitfehler. Wenn Sie diesen Fehler mit unique_ptr machen, erhalten Sie einen Kompilierzeitfehler. Und , dass ist der Unterschied zwischen auto_ptr und unique_ptr.

Um den Kompilierzeitfehler zu beheben, müssen Sie ~T1() umreißen, nachdem T2 abgeschlossen ist. In test2.cpp abgeben T2:

T1::~T1() = default; 

Nun sollte es kompilieren und Ausgang:

~T2() 

Sie wahrscheinlich auch erklären und Kontur bewegen Mitglieder werden wollen:

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

Sie könnte diese gleichen Korrekturen mit auto_ptr machen und es wäre wieder korrekt. Aber wieder, der Unterschied zwischen auto_ptr und unique_ptr ist, dass mit ersteren, Sie bis zur Laufzeit nicht herausfinden, dass Sie einige Debuggen zu tun haben (modulo optionale Warnungen, die Ihr Compiler geben kann). Mit letzterem wirst du garantiert zur Kompilierzeit herausfinden.