Ist std :: unique_ptr <T> erforderlich, um die vollständige Definition von T zu kennen?

248

Ich habe einen Code in einem Header, der so aussieht:

#include <memory>

class Thing;

class MyClass
{
    std::unique_ptr< Thing > my_thing;
};

Wenn ich diesen Header in eine ThingCPP einbinde, die die Typdefinition nicht enthält , wird dies unter VS2010-SP1 nicht kompiliert:

1> C: \ Programme (x86) \ Microsoft Visual Studio 10.0 \ VC \ include \ memory (2067): Fehler C2027: Verwendung des undefinierten Typs 'Thing'

Ersetzen std::unique_ptrdurch std::shared_ptrund es wird kompiliert.

Ich vermute also, dass es die aktuelle std::unique_ptrImplementierung von VS2010 ist , die die vollständige Definition erfordert und vollständig implementierungsabhängig ist.

Oder ist es? Gibt es etwas in den Standardanforderungen, das es std::unique_ptrder Implementierung unmöglich macht, nur mit einer Vorwärtsdeklaration zu arbeiten? Es fühlt sich seltsam an, da es nur einen Zeiger enthalten Thingsollte, nicht wahr?

Klaim
quelle
20
Die beste Erklärung dafür, wann Sie mit den intelligenten C ++ 0x-Zeigern einen vollständigen Typ benötigen und wann nicht, ist Howard Hinnants "Unvollständige Typen und shared_ptr/ unique_ptr". Die Tabelle am Ende sollte Ihre Frage beantworten.
James McNellis
17
Danke für den Hinweis James. Ich hatte vergessen, wo ich diesen Tisch hingestellt hatte! :-)
Howard Hinnant
5
@JamesMcNellis Der Link zur Website von Howard Hinnant ist nicht verfügbar. Hier ist die web.archive.org-Version davon. Auf jeden Fall hat er es unten perfekt mit dem gleichen Inhalt beantwortet :-)
Ela782
Eine weitere gute Erklärung findet sich in Punkt 22 von Scott Meyers 'Effective Modern C ++.
Fred Schoen

Antworten:

328

Von hier aus adoptiert .

Die meisten Vorlagen in der C ++ - Standardbibliothek erfordern, dass sie mit vollständigen Typen instanziiert werden. Allerdings shared_ptrund unique_ptrsind teilweise Ausnahmen. Einige, aber nicht alle Mitglieder können mit unvollständigen Typen instanziiert werden. Die Motivation dafür ist, Redewendungen wie Pickel mit intelligenten Zeigern zu unterstützen, ohne undefiniertes Verhalten zu riskieren.

Undefiniertes Verhalten kann auftreten, wenn Sie einen unvollständigen Typ haben und ihn aufrufen delete:

class A;
A* a = ...;
delete a;

Das obige ist ein gesetzlicher Code. Es wird kompiliert. Ihr Compiler gibt möglicherweise eine Warnung für den obigen Code wie den oben genannten aus. Wenn es ausgeführt wird, werden wahrscheinlich schlimme Dinge passieren. Wenn Sie sehr viel Glück haben, stürzt Ihr Programm ab. Ein wahrscheinlicheres Ergebnis ist jedoch, dass Ihr Programm stillschweigend Speicher verliert, da ~A()es nicht aufgerufen wird.

Die Verwendung auto_ptr<A>im obigen Beispiel hilft nicht. Sie erhalten immer noch das gleiche undefinierte Verhalten, als hätten Sie einen Rohzeiger verwendet.

Trotzdem ist es sehr nützlich, an bestimmten Stellen unvollständige Klassen zu verwenden! Dies ist wo shared_ptrund unique_ptrhelfen. Wenn Sie einen dieser intelligenten Zeiger verwenden, können Sie mit einem unvollständigen Typ davonkommen, es sei denn, es ist ein vollständiger Typ erforderlich. Und am wichtigsten ist, dass Sie einen Fehler beim Kompilieren erhalten, wenn Sie versuchen, den Smart Pointer mit einem unvollständigen Typ zu diesem Zeitpunkt zu verwenden, wenn ein vollständiger Typ erforderlich ist.

Kein undefiniertes Verhalten mehr:

Wenn Ihr Code kompiliert wird, haben Sie überall einen vollständigen Typ verwendet, den Sie benötigen.

class A
{
    class impl;
    std::unique_ptr<impl> ptr_;  // ok!

public:
    A();
    ~A();
    // ...
};

shared_ptrund unique_ptrerfordern einen vollständigen Typ an verschiedenen Stellen. Die Gründe sind unklar und haben mit einem dynamischen Löscher gegenüber einem statischen Löscher zu tun. Die genauen Gründe sind nicht wichtig. Tatsächlich ist es in den meisten Codes nicht wirklich wichtig, dass Sie genau wissen, wo ein vollständiger Typ erforderlich ist. Nur Code, und wenn Sie es falsch verstehen, wird der Compiler es Ihnen sagen.

Falls es für Sie jedoch hilfreich ist, finden Sie hier eine Tabelle, in der mehrere Mitglieder shared_ptrund unique_ptrin Bezug auf Vollständigkeitsanforderungen dokumentiert sind. Wenn das Mitglied einen vollständigen Typ benötigt, hat der Eintrag ein "C", andernfalls wird der Tabelleneintrag mit "I" gefüllt.

Complete type requirements for unique_ptr and shared_ptr

                            unique_ptr       shared_ptr
+------------------------+---------------+---------------+
|          P()           |      I        |      I        |
|  default constructor   |               |               |
+------------------------+---------------+---------------+
|      P(const P&)       |     N/A       |      I        |
|    copy constructor    |               |               |
+------------------------+---------------+---------------+
|         P(P&&)         |      I        |      I        |
|    move constructor    |               |               |
+------------------------+---------------+---------------+
|         ~P()           |      C        |      I        |
|       destructor       |               |               |
+------------------------+---------------+---------------+
|         P(A*)          |      I        |      C        |
+------------------------+---------------+---------------+
|  operator=(const P&)   |     N/A       |      I        |
|    copy assignment     |               |               |
+------------------------+---------------+---------------+
|    operator=(P&&)      |      C        |      I        |
|    move assignment     |               |               |
+------------------------+---------------+---------------+
|        reset()         |      C        |      I        |
+------------------------+---------------+---------------+
|       reset(A*)        |      C        |      C        |
+------------------------+---------------+---------------+

Alle Operationen, die Zeigerkonvertierungen erfordern, erfordern vollständige Typen für unique_ptrund shared_ptr.

Der unique_ptr<A>{A*}Konstruktor kann Anur dann mit einer Unvollständigkeit davonkommen, wenn der Compiler keinen Aufruf an einrichten muss ~unique_ptr<A>(). Wenn Sie zum Beispiel das unique_ptrauf den Haufen legen , können Sie mit einem unvollständigen davonkommen A. Weitere Details zu diesem Punkt finden Sie in der Antwort von BarryTheHatchet hier .

Howard Hinnant
quelle
3
Hervorragende Antwort. Ich würde es +5, wenn ich könnte. Ich bin sicher, ich werde in meinem nächsten Projekt darauf zurückgreifen, in dem ich versuche, intelligente Zeiger voll auszunutzen.
Matthias
4
Wenn man erklären kann, was die Tabelle bedeutet, wird sie wahrscheinlich mehr Menschen helfen
Ghita
8
Noch ein Hinweis: Ein Klassenkonstruktor verweist auf die Destruktoren seiner Mitglieder (für den Fall, dass eine Ausnahme ausgelöst wird, müssen diese Destruktoren aufgerufen werden). Während der Destruktor von unique_ptr einen vollständigen Typ benötigt, reicht es nicht aus, einen benutzerdefinierten Destruktor in einer Klasse zu haben - er benötigt auch einen Konstruktor.
Johannes Schaub - litb
7
@Mehrdad: Diese Entscheidung wurde für C ++ 98 getroffen, was vor meiner Zeit liegt. Ich glaube jedoch, dass die Entscheidung auf Bedenken hinsichtlich der Implementierbarkeit und der Schwierigkeit der Spezifikation zurückzuführen ist (dh genau, welche Teile eines Containers einen vollständigen Typ erfordern oder nicht). Selbst heute, mit 15 Jahren Erfahrung seit C ++ 98, wäre es keine triviale Aufgabe, sowohl die Containerspezifikation in diesem Bereich zu lockern als auch sicherzustellen, dass Sie wichtige Implementierungstechniken oder Optimierungen nicht verbieten. Ich denke, es könnte getan werden. Ich weiß, es wäre viel Arbeit. Mir ist bekannt, dass eine Person den Versuch unternimmt.
Howard Hinnant
9
Weil es nicht offensichtlich aus den obigen Ausführungen , für alle mit diesem Problem , weil sie ein definieren unique_ptrals Membervariable einer Klasse, nur explizit deklarieren eine destructor (und Konstruktor) in der Klassendeklaration (in der Header - Datei) und gehen Sie zu definieren , sie in der Quelldatei (und fügen Sie den Header mit der vollständigen Deklaration der Klasse, auf die verwiesen wird, in die Quelldatei ein), um zu verhindern, dass der Compiler den Konstruktor oder Destruktor automatisch in die Headerdatei einfügt (was den Fehler auslöst). stackoverflow.com/a/13414884/368896 erinnert mich auch daran.
Dan Nissenbaum
42

Der Compiler benötigt die Definition von Thing, um den Standarddestruktor für MyClass zu generieren. Wenn Sie den Destruktor explizit deklarieren und seine (leere) Implementierung in die CPP-Datei verschieben, sollte der Code kompiliert werden.

Igor Nazarenko
quelle
5
Ich denke, dies ist die perfekte Gelegenheit, eine Standardfunktion zu verwenden. MyClass::~MyClass() = default;In der Implementierungsdatei scheint es weniger wahrscheinlich zu sein, dass sie später versehentlich von jemandem entfernt wird, der annimmt, dass der Destruktorkörper gelöscht wurde, anstatt absichtlich leer gelassen zu werden.
Dennis Zickefoose
@ Tennis Zickefoose: Leider verwendet das OP VC ++ und VC ++ unterstützt noch keine Mitglieder der Klassen defaulted und deleted.
ildjarn
6
+1 für das Verschieben der Tür in die CPP-Datei. Außerdem scheint MyClass::~MyClass() = defaultes nicht in die Implementierungsdatei von Clang verschoben zu werden. (noch?)
Eonil
Sie müssen auch die Implementierung des Konstruktors in die CPP-Datei verschieben, zumindest in VS 2017. Siehe zum Beispiel diese Antwort: stackoverflow.com/a/27624369/5124002
jciloa
15

Dies ist nicht implementierungsabhängig. Der Grund dafür ist, dass shared_ptrder richtige Destruktor zur Laufzeit ermittelt wird - er ist nicht Teil der Typensignatur. Der unique_ptrDestruktor ist jedoch Teil seines Typs und muss zur Kompilierungszeit bekannt sein.

Hündchen
quelle
8

Es sieht so aus, als ob die aktuellen Antworten nicht genau festlegen, warum der Standardkonstruktor (oder Destruktor) ein Problem darstellt, leere Antworten, die in cpp deklariert sind, jedoch nicht.

Folgendes passiert:

Wenn die äußere Klasse (dh MyClass) keinen Konstruktor oder Destruktor hat, generiert der Compiler die Standardklassen. Das Problem dabei ist, dass der Compiler im Wesentlichen den standardmäßigen leeren Konstruktor / Destruktor in die .hpp-Datei einfügt. Dies bedeutet, dass der Code für den Standard-Konstruktor / Destruktor zusammen mit der Binärdatei der ausführbaren Host-Datei kompiliert wird, nicht zusammen mit den Binärdateien Ihrer Bibliothek. Diese Definitionen können jedoch die Teilklassen nicht wirklich konstruieren. Wenn der Linker in die Binärdatei Ihrer Bibliothek wechselt und versucht, den Konstruktor / Destruktor abzurufen, findet er keinen und Sie erhalten einen Fehler. Wenn sich der Konstruktor- / Destruktorcode in Ihrer CPP befand, steht in Ihrer Bibliotheksbinärdatei der Code zum Verknüpfen zur Verfügung.

Dies hat nichts mit der Verwendung von unique_ptr oder shared_ptr zu tun, und andere Antworten scheinen ein verwirrender Fehler in der alten VC ++ - Implementierung von unique_ptr zu sein (VC ++ 2015 funktioniert auf meinem Computer einwandfrei).

Die Moral der Geschichte ist also, dass Ihr Header frei von Konstruktor / Destruktor-Definitionen bleiben muss. Es kann nur ihre Erklärung enthalten. Zum Beispiel ~MyClass()=default;funktioniert in hpp nicht. Wenn Sie dem Compiler erlauben, einen Standardkonstruktor oder -destruktor einzufügen, wird ein Linkerfehler angezeigt.

Eine weitere Randnotiz: Wenn Sie diesen Fehler auch dann noch erhalten, wenn Sie Konstruktor und Destruktor in der CPP-Datei haben, liegt der Grund höchstwahrscheinlich darin, dass Ihre Bibliothek nicht ordnungsgemäß kompiliert wird. Zum Beispiel habe ich einmal einfach den Projekttyp in VC ++ von "Konsole" in "Bibliothek" geändert und diesen Fehler erhalten, weil VC ++ kein _LIB-Präprozessorsymbol hinzugefügt hat und genau dieselbe Fehlermeldung ausgegeben hat.

Shital Shah
quelle
Danke dir! Das war eine sehr prägnante Erklärung für eine unglaublich obskure C ++ - Eigenart. Hat mir viel Ärger erspart.
JPNotADragon
4

Nur der Vollständigkeit halber:

Header: Ah

class B; // forward declaration

class A
{
    std::unique_ptr<B> ptr_;  // ok!  
public:
    A();
    ~A();
    // ...
};

Quelle A.cpp:

class B {  ...  }; // class definition

A::A() { ... }
A::~A() { ... }

Die Definition von Klasse B muss von Konstruktor, Destruktor und allem gesehen werden, was B implizit löschen könnte. (Obwohl der Konstruktor in der obigen Liste nicht aufgeführt ist, benötigt in VS2017 sogar der Konstruktor die Definition von B. Und dies ist sinnvoll, wenn man dies berücksichtigt dass im Falle einer Ausnahme im Konstruktor das unique_ptr erneut zerstört wird.)

Joachim
quelle
1

Die vollständige Definition der Sache ist zum Zeitpunkt der Instanziierung der Vorlage erforderlich. Dies ist der genaue Grund, warum das Pimpl-Idiom kompiliert wird.

Wenn es nicht möglich war, die Leute bitten, würde nicht Fragen wie diese .

BЈовић
quelle
-2

Die einfache Antwort lautet stattdessen einfach shared_ptr.

Deltanin
quelle
-7

Was mich betrifft,

QList<QSharedPointer<ControllerBase>> controllers;

Fügen Sie einfach den Header hinzu ...

#include <QSharedPointer>
Sanbrother
quelle
Antwort nicht verwandt und nicht relevant für die Frage.
Mikus