Est-ce que std :: unique_ptr <T> doit connaître la définition complète de T?

248

J'ai du code dans un en-tête qui ressemble à ceci:

#include <memory>

class Thing;

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

Si Thingj'inclus cet en-tête dans un cpp qui n'inclut pas la définition de type, cela ne se compile pas sous VS2010-SP1:

1> C: \ Program Files (x86) \ Microsoft Visual Studio 10.0 \ VC \ include \ memory (2067): erreur C2027: utilisation du type non défini 'Thing'

Remplacez std::unique_ptrpar std::shared_ptret il compile.

Donc, je suppose que c'est l'implémentation actuelle du VS2010 std::unique_ptrqui nécessite la définition complète et est totalement dépendante de l'implémentation.

Ou est-ce? Y a-t-il quelque chose dans ses exigences standard qui rend impossible std::unique_ptrla mise en œuvre de 's avec une déclaration directe uniquement? Cela semble étrange car il ne devrait contenir qu'un pointeur Thing, n'est-ce pas?

Klaim
la source
20
La meilleure explication du moment ou vous n'avez pas besoin d'un type complet avec les pointeurs intelligents C ++ 0x est "Types incomplets et shared_ptr/ unique_ptr" de Howard Hinnant. Le tableau à la fin devrait répondre à votre question.
James McNellis
17
Merci pour le pointeur James. J'avais oublié où j'avais mis cette table! :-)
Howard Hinnant
5
@JamesMcNellis Le lien vers le site Web de Howard Hinnant est en baisse. En voici la version web.archive.org . En tout cas, il y a répondu parfaitement ci-dessous avec le même contenu :-)
Ela782
Une autre bonne explication est donnée au point 22 du document C ++ moderne efficace de Scott Meyers.
Fred Schoen

Réponses:

328

Adopté d' ici .

La plupart des modèles de la bibliothèque standard C ++ nécessitent qu'ils soient instanciés avec des types complets. Cependant shared_ptret unique_ptrsont des exceptions partielles . Certains, mais pas tous leurs membres peuvent être instanciés avec des types incomplets. La motivation pour cela est de supporter des idiomes tels que pimpl en utilisant des pointeurs intelligents, et sans risquer un comportement indéfini.

Un comportement indéfini peut se produire lorsque vous avez un type incomplet et que vous l'appelez delete:

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

Ce qui précède est un code légal. Il compilera. Votre compilateur peut ou non émettre un avertissement pour le code ci-dessus comme ci-dessus. Lors de son exécution, de mauvaises choses se produiront probablement. Si vous êtes très chanceux, votre programme se bloquera. Cependant, un résultat plus probable est que votre programme perdra silencieusement de la mémoire car ~A()il ne sera pas appelé.

L'utilisation auto_ptr<A>de l'exemple ci-dessus n'aide pas. Vous obtenez toujours le même comportement indéfini que si vous aviez utilisé un pointeur brut.

Néanmoins, l'utilisation de classes incomplètes à certains endroits est très utile! C'est là shared_ptret unique_ptraider. L'utilisation de l'un de ces pointeurs intelligents vous permettra de vous en sortir avec un type incomplet, sauf lorsqu'il est nécessaire d'avoir un type complet. Et surtout, lorsqu'il est nécessaire d'avoir un type complet, vous obtenez une erreur de compilation si vous essayez d'utiliser le pointeur intelligent avec un type incomplet à ce stade.

Plus de comportement indéfini:

Si votre code compile, vous avez utilisé un type complet partout où vous en avez besoin.

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

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

shared_ptret unique_ptrnécessitent un type complet à différents endroits. Les raisons sont obscures, ayant à voir avec un deleter dynamique vs un deleter statique. Les raisons précises ne sont pas importantes. En fait, dans la plupart des codes, il n'est pas vraiment important que vous sachiez exactement où un type complet est requis. Juste du code, et si vous vous trompez, le compilateur vous le dira.

Cependant, au cas où cela vous serait utile, voici un tableau qui documente plusieurs membres shared_ptret unique_ptrconcernant les exigences d'exhaustivité. Si le membre requiert un type complet, l'entrée a un "C", sinon l'entrée de table est remplie de "I".

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

Toutes les opérations nécessitant des conversions de pointeurs nécessitent des types complets pour unique_ptret shared_ptr.

Le unique_ptr<A>{A*}constructeur ne peut s'en tirer avec un incomplet Aque si le compilateur n'est pas obligé d'établir un appel à ~unique_ptr<A>(). Par exemple, si vous mettez le unique_ptrsur le tas, vous pouvez vous en sortir avec un incomplet A. Plus de détails sur ce point peuvent être trouvés dans la réponse de BarryTheHatchet ici .

Howard Hinnant
la source
3
Excellente réponse. Je le ferais +5 si je le pouvais. Je suis sûr que j'y reviendrai dans mon prochain projet, dans lequel j'essaie d'utiliser pleinement les pointeurs intelligents.
matthias
4
si l'on peut expliquer ce que signifie le tableau, je suppose que cela aidera plus de gens
Ghita
8
Une dernière remarque: un constructeur de classe référencera les destructeurs de ses membres (dans le cas où une exception est levée, ces destructeurs doivent être appelés). Ainsi, alors que le destructeur de unique_ptr a besoin d'un type complet, il ne suffit pas d'avoir un destructeur défini par l'utilisateur dans une classe - il a également besoin d'un constructeur.
Johannes Schaub - litb
7
@Mehrdad: Cette décision a été prise pour C ++ 98, ce qui est avant mon temps. Cependant, je crois que la décision est venue d'un souci d'implémentabilité et de la difficulté de spécification (c'est-à-dire exactement quelles parties d'un conteneur nécessitent ou ne nécessitent pas un type complet). Même aujourd'hui, avec 15 ans d'expérience depuis C ++ 98, ce serait une tâche non triviale à la fois d'assouplir la spécification du conteneur dans ce domaine et de s'assurer que vous ne proscriviez pas les techniques d'implémentation ou les optimisations importantes. Je pense que cela pourrait être fait. Je sais que ce serait beaucoup de travail. Je connais une personne qui a tenté.
Howard Hinnant
9
Parce que ce n'est pas évident d'après les commentaires ci - dessus, pour toute personne ayant ce problème parce qu'ils définissent un unique_ptrcomme une variable membre d'une classe, juste explicitement déclarer un destructor (et constructeur) dans la déclaration de classe (dans le fichier d' en- tête) et procéder à définir les dans le fichier source (et placez l'en-tête avec la déclaration complète de la classe pointée dans le fichier source) pour empêcher le compilateur d'inscrire automatiquement le constructeur ou le destructeur dans le fichier d'en-tête (ce qui déclenche l'erreur). stackoverflow.com/a/13414884/368896 aide également à me le rappeler.
Dan Nissenbaum
42

Le compilateur a besoin de la définition de Thing pour générer le destructeur par défaut pour MyClass. Si vous déclarez explicitement le destructeur et déplacez son implémentation (vide) dans le fichier CPP, le code doit être compilé.

Igor Nazarenko
la source
5
Je pense que c'est l'occasion idéale d'utiliser une fonction par défaut. MyClass::~MyClass() = default;dans le fichier d'implémentation semble moins susceptible d'être supprimé par inadvertance plus tard sur la route par quelqu'un qui suppose que le corps du destructeur a été effacé plutôt que laissé délibérément en blanc.
Dennis Zickefoose
@Dennis Zickefoose: Malheureusement, l'OP utilise VC ++, et VC ++ ne prend pas encore en charge les membres des classes defaulted et deleted.
ildjarn
6
+1 pour savoir comment déplacer la porte dans un fichier .cpp. Il semble également qu'il MyClass::~MyClass() = defaultne le déplace pas dans le fichier d'implémentation sur Clang. (encore?)
Eonil
Vous devez également déplacer l'implémentation du constructeur vers le fichier CPP, au moins sur VS 2017. Voir par exemple cette réponse: stackoverflow.com/a/27624369/5124002
jciloa
15

Cela ne dépend pas de l'implémentation. La raison pour laquelle cela fonctionne est parce qu'il shared_ptrdétermine le destructeur correct à appeler au moment de l'exécution - il ne fait pas partie de la signature de type. Cependant, unique_ptrle destructeur de 's fait partie de son type, et il doit être connu au moment de la compilation.

Chiot
la source
8

Il semble que les réponses actuelles ne déterminent pas exactement pourquoi le constructeur (ou le destructeur) par défaut pose problème, mais pas les réponses vides déclarées dans cpp.

Voici ce qui se passe:

Si la classe externe (c'est-à-dire MyClass) n'a pas de constructeur ou de destructeur, alors le compilateur génère ceux par défaut. Le problème est que le compilateur insère essentiellement le constructeur / destructeur vide par défaut dans le fichier .hpp. Cela signifie que le code du constructeur / destructeur par défaut est compilé avec le binaire de l'exécutable hôte, pas avec les binaires de votre bibliothèque. Cependant, ces définitions ne peuvent pas vraiment construire les classes partielles. Ainsi, lorsque l'éditeur de liens va dans le binaire de votre bibliothèque et essaie d'obtenir le constructeur / destructeur, il n'en trouve aucun et vous obtenez une erreur. Si le code constructeur / destructeur était dans votre .cpp, alors votre binaire de bibliothèque a celui-ci disponible pour la liaison.

Cela n'a rien à voir avec l'utilisation de unique_ptr ou shared_ptr et d'autres réponses semblent être un bogue confondant possible dans l'ancien VC ++ pour la mise en œuvre unique_ptr (VC ++ 2015 fonctionne très bien sur ma machine).

La morale de l'histoire est que votre en-tête doit rester libre de toute définition de constructeur / destructeur. Il ne peut contenir que leur déclaration. Par exemple, ~MyClass()=default;en hpp ne fonctionnera pas. Si vous autorisez le compilateur à insérer le constructeur ou le destructeur par défaut, vous obtiendrez une erreur de l'éditeur de liens.

Une autre note latérale: Si vous obtenez toujours cette erreur même après avoir le constructeur et le destructeur dans le fichier cpp, la raison est probablement que votre bibliothèque n'est pas correctement compilée. Par exemple, une fois, j'ai simplement changé le type de projet de la console en bibliothèque dans VC ++ et j'ai eu cette erreur car VC ++ n'a pas ajouté le symbole du préprocesseur _LIB et cela a produit exactement le même message d'erreur.

Shital Shah
la source
Je vous remercie! C'était une explication très succincte d'une bizarrerie C ++ incroyablement obscure. Cela m'a sauvé beaucoup de problèmes.
JPNotADragon
5

Juste pour être complet:

En-tête: Ah

class B; // forward declaration

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

Source A.cpp:

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

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

La définition de la classe B doit être vue par le constructeur, le destructeur et tout ce qui pourrait implicitement supprimer B. (Bien que le constructeur n'apparaisse pas dans la liste ci-dessus, dans VS2017 même le constructeur a besoin de la définition de B. qu'en cas d'exception dans le constructeur, unique_ptr est à nouveau détruit.)

Joachim
la source
1

La définition complète de la chose est requise au moment de l'instanciation du modèle. C'est la raison exacte pour laquelle l'idiome pimpl se compile.

Si ce n'était pas possible, les gens ne poseraient pas de questions comme celle-ci .

BЈовић
la source
-2

La réponse simple est d'utiliser à la place shared_ptr.

deltanine
la source
-7

Comme pour moi,

QList<QSharedPointer<ControllerBase>> controllers;

Il suffit d'inclure l'en-tête ...

#include <QSharedPointer>
Sanbrother
la source
Réponse non liée et non pertinente à la question.
Mikus