web-dev-qa-db-fra.com

Std :: unique_ptr <T> est-il tenu de connaître la définition complète de T?

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

#include <memory>

class Thing;

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

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

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

Remplacez std::unique_ptr Par std::shared_ptr Et cela se compile.

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

Ou est-ce? Y a-t-il quelque chose dans ses exigences standard qui empêche la mise en œuvre de std::unique_ptr De fonctionner uniquement avec une déclaration anticipée? Cela semble étrange, car il ne devrait contenir qu'un pointeur sur Thing, n'est-ce pas?

230
Klaim

Adopté à partir de 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_ptr Et unique_ptr Sont des partiels exceptions. Certains membres, mais pas tous, peuvent être instanciés avec des types incomplets. La motivation à cela est de prendre en charge des idiomes tels que pimpl en utilisant des pointeurs intelligents, et sans risquer de comportement non défini.

Un comportement non défini peut se produire lorsque vous avez un type incomplet et que vous appelez delete dessus:

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

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

L'utilisation de auto_ptr<A> Dans 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, utiliser des classes incomplètes à certains endroits est très utile! C'est là que shared_ptr Et unique_ptr Aident. L'utilisation de l'un de ces pointeurs intelligents vous laissera sortir avec un type incomplet, sauf s'il est nécessaire d'avoir un type complet. Et surtout, quand 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 aucun comportement indéfini:

Si votre code est compilé, 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_ptr Et unique_ptr Requièrent un type complet à différents endroits. Les raisons sont obscures, qu’il s’agisse d’un deleter dynamique ou d’un deleter statique. Les raisons précises ne sont pas importantes. En fait, dans la plupart des codes, il n’est pas vraiment important pour vous de savoir 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 de shared_ptr Et unique_ptr En ce qui concerne les exigences relatives à l'exhaustivité. Si le membre requiert un type complet, l'entrée a un "C", sinon l'entrée du tableau est remplie avec "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        |
+------------------------+---------------+---------------+

Toute opération nécessitant une conversion de pointeur nécessite des types complets pour unique_ptr Et shared_ptr.

Le constructeur unique_ptr<A>{A*} Peut s'en sortir avec un A incomplet que si le compilateur n'est pas obligé de configurer un appel à ~unique_ptr<A>(). Par exemple, si vous mettez le unique_ptr Sur le tas, vous pouvez vous en sortir avec un A incomplet. On trouvera plus de détails sur ce point dans BarryTheHatchet's answer here .

311
Howard Hinnant

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

41
Igor Nazarenko

Ce n'est pas dépendant de l'implémentation. Cela fonctionne parce que shared_ptr Détermine le destructeur correct à appeler au moment de l'exécution - il ne fait pas partie de la signature de type. Cependant, le destructeur de unique_ptrest fait partie de son type et doit être connu à la compilation.

14
Puppy

Il semble que les réponses actuelles ne permettent pas de comprendre 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, le compilateur génère les classes par défaut. Le problème avec ceci 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 fichier binaire de l'exécutable hôte, et non avec les fichiers binaires de votre bibliothèque. Cependant, ces définitions ne peuvent pas vraiment construire les classes partielles. Ainsi, lorsque l'éditeur de liens entre dans le fichier binaire de votre bibliothèque et tente d'obtenir le constructeur/destructeur, il n'en trouve aucun et vous obtenez une erreur. Si le code constructeur/destructeur se trouvait dans votre fichier .cpp, le binaire de votre bibliothèque en disposera pour la liaison.

Cela n'a rien à voir avec unique_ptr ou shared_ptr et d'autres réponses semblent être un bogue déroutant dans l'ancien VC++ pour l'implémentation unique_ptr (VC++ 2015 fonctionne bien sur ma machine).

La moralité de l’histoire est donc 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; dans 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.

Autre remarque: si vous obtenez toujours cette erreur même après que vous ayez un constructeur et un destructeur dans le fichier cpp, la raison en est probablement que votre bibliothèque n'est pas compilée correctement. Par exemple, une fois, j’ai simplement changé de type de projet de console en bibliothèque dans VC++ et cette erreur s’est produite, car VC++ n’a pas ajouté le symbole de préprocesseur _LIB et a généré le même message d’erreur.

6
Shital Shah

Juste pour être complet:

En-tête: A.h

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 par tout ce qui pourrait implicitement supprimer B. qu’en cas d’exception dans le constructeur, l’unique_ptr soit détruit à nouveau.)

2
Joachim

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 le langage Pimpl est compilé.

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

1
BЈовић

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

0
deltanine