std :: unique_ptr avec un type incomplet ne compilera pas

203

J'utilise le pimpl-idiom avec std::unique_ptr:

class window {
  window(const rectangle& rect);

private:
  class window_impl; // defined elsewhere
  std::unique_ptr<window_impl> impl_; // won't compile
};

Cependant, j'obtiens une erreur de compilation concernant l'utilisation d'un type incomplet, à la ligne 304 dans <memory>:

Application non valide de ' sizeof' à un type incomplet ' uixx::window::window_impl'

Pour autant que je sache, std::unique_ptrdevrait pouvoir être utilisé avec un type incomplet. Est-ce un bogue dans libc ++ ou est-ce que je fais quelque chose de mal ici?


la source
Lien de référence pour les exigences d'exhaustivité: stackoverflow.com/a/6089065/576911
Howard Hinnant
1
Un bouton est souvent construit et non modifié depuis. J'utilise habituellement un std :: shared_ptr <const window_impl>
mfnx
Connexes: Je voudrais bien savoir pourquoi cela fonctionne dans MSVC et comment l'empêcher de fonctionner (afin que je ne casse pas les compilations de mes collègues GCC).
Len

Réponses:

258

Voici quelques exemples de std::unique_ptrtypes incomplets. Le problème réside dans la destruction.

Si vous utilisez pimpl avec unique_ptr, vous devez déclarer un destructeur:

class foo
{ 
    class impl;
    std::unique_ptr<impl> impl_;

public:
    foo(); // You may need a def. constructor to be defined elsewhere

    ~foo(); // Implement (with {}, or with = default;) where impl is complete
};

car sinon le compilateur en génère un par défaut, et il a besoin d'une déclaration complète de foo::impl pour cela.

Si vous avez des constructeurs de modèles, vous êtes vissé, même si vous ne construisez pas le impl_membre:

template <typename T>
foo::foo(T bar) 
{
    // Here the compiler needs to know how to
    // destroy impl_ in case an exception is
    // thrown !
}

Au niveau de l'espace de noms, l'utilisation unique_ptrne fonctionnera pas non plus:

class impl;
std::unique_ptr<impl> impl_;

puisque le compilateur doit savoir ici comment détruire cet objet de durée statique. Une solution de contournement est:

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;
Alexandre C.
la source
3
Je trouve que votre première solution (en ajoutant le destructeur foo ) permet de compiler la déclaration de classe elle-même, mais déclarer un objet de ce type n'importe où entraîne l'erreur d'origine ("application invalide de 'sizeof' ...").
Jeff Trull
38
excellente réponse, juste pour noter; nous pouvons toujours utiliser le constructeur / destructeur par défaut en plaçant par exemple foo::~foo() = default;dans le fichier src
assem
2
Une façon de vivre avec les constructeurs de modèles serait de déclarer mais pas de définir le constructeur dans le corps de classe, de le définir quelque part où la définition complète de l'impl est vue et d'instancier explicitement toutes les instanciations nécessaires.
enobayram
2
Pourriez-vous expliquer comment cela fonctionnerait dans certains cas et pas dans d'autres? J'ai utilisé l'idiome pimpl avec un unique_ptr et une classe sans destructeur, et dans un autre projet, mon code ne parvient pas à se compiler avec l'erreur OP mentionnée ..
Curieux
1
Il semble que si la valeur par défaut de unique_ptr est définie sur {nullptr} dans le fichier d'en-tête de la classe avec le style c ++ 11, une déclaration complète est également nécessaire pour la raison ci-dessus.
feirainy
53

Comme l'a mentionné Alexandre C. , le problème se résume à windowla définition implicite du destructeur dans des endroits où le type de window_implest encore incomplet. En plus de ses solutions, une autre solution de contournement que j'ai utilisée consiste à déclarer un foncteur Deleter dans l'en-tête:

// Foo.h

class FooImpl;
struct FooImplDeleter
{
  void operator()(FooImpl *p);
};

class Foo
{
...
private:
  std::unique_ptr<FooImpl, FooImplDeleter> impl_;
};

// Foo.cpp

...
void FooImplDeleter::operator()(FooImpl *p)
{
  delete p;
}

Notez que l'utilisation d'une fonction Deleter personnalisée empêche l'utilisation de std::make_unique(disponible à partir de C ++ 14), comme déjà discuté ici .

Fernando Costa Bertoldi
la source
6
Pour moi, c'est la bonne solution. Ce n'est pas unique à l'utilisation de l'idiome pimpl, c'est un problème général avec l'utilisation de std :: unique_ptr avec des classes incomplètes. Le deleter par défaut utilisé par std :: unique_ptr <X> tente de faire "supprimer X", ce qu'il ne peut pas faire si X est une déclaration directe. En spécifiant une fonction deleter, vous pouvez placer cette fonction dans un fichier source où la classe X est complètement définie. D'autres fichiers source peuvent alors utiliser std :: unique_ptr <X, DeleterFunc> même si X n'est qu'une déclaration directe tant qu'ils sont liés au fichier source contenant DeleterFunc.
sheltond
1
Il s'agit d'une bonne solution de contournement lorsque vous devez avoir une définition de fonction en ligne créant une instance de votre type "Foo" (par exemple une méthode statique "getInstance" qui référence le constructeur et le destructeur), et que vous ne voulez pas les déplacer dans un fichier d'implémentation comme le suggère @ adspx5.
GameSalutes
20

utiliser un suppresseur personnalisé

Le problème est que unique_ptr<T>doit appeler le destructeur T::~T()dans son propre destructeur, son opérateur d'affectation de déplacement et la unique_ptr::reset()fonction membre (uniquement). Cependant, ceux-ci doivent être appelés (implicitement ou explicitement) dans plusieurs situations PIMPL (déjà dans le destructeur de la classe externe et l'opérateur d'affectation de déplacement).

Comme nous l' avons souligné dans une autre réponse, une façon d'éviter que est de déplacer toutes les opérations qui nécessitent unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)etunique_ptr::reset() dans le fichier source où la classe d'aide de Pimpl est réellement défini.

Cependant, cela est plutôt gênant et défie le point même de l'idiome de bouton à un certain degré. Une solution beaucoup plus propre qui évite tout ce qui est d'utiliser un deleter personnalisé et à ne déplacer sa définition que dans le fichier source où réside la classe d'assistance de bouton. Voici un exemple simple:

// file.h
class foo
{
  struct pimpl;
  struct pimpl_deleter { void operator()(pimpl*) const; };
  std::unique_ptr<pimpl,pimpl_deleter> m_pimpl;
public:
  foo(some data);
  foo(foo&&) = default;             // no need to define this in file.cc
  foo&operator=(foo&&) = default;   // no need to define this in file.cc
//foo::~foo()          auto-generated: no need to define this in file.cc
};

// file.cc
struct foo::pimpl
{
  // lots of complicated code
};
void foo::pimpl_deleter::operator()(foo::pimpl*ptr) const { delete ptr; }

Au lieu d'une classe deleter distincte, vous pouvez également utiliser une fonction libre ou un staticmembre de fooconjointement avec un lambda:

class foo {
  struct pimpl;
  static void delete_pimpl(pimpl*);
  std::unique_ptr<pimpl,[](pimpl*ptr){delete_pimpl(ptr);}> m_pimpl;
};
Walter
la source
15

Vous avez probablement des corps de fonction dans le fichier .h de la classe qui utilisent un type incomplet.

Assurez-vous que dans votre fenêtre .h pour la classe, vous n'avez que la déclaration de fonction. Tous les corps de fonction pour la fenêtre doivent être dans un fichier .cpp. Et pour window_impl aussi ...

Btw, vous devez ajouter explicitement la déclaration du destructeur pour la classe windows dans votre fichier .h.

Mais vous NE POUVEZ PAS mettre de corps de dtor vide dans votre fichier d'en-tête:

class window {
    virtual ~window() {};
  }

Doit être juste une déclaration:

  class window {
    virtual ~window();
  }
adspx5
la source
C'était aussi ma solution. Beaucoup plus concis. Ayez simplement votre constructeur / destructeur déclaré dans l'en-tête et défini dans le fichier cpp.
Kris Morness
2

Pour ajouter aux réponses des autres sur le suppresseur personnalisé, dans notre "bibliothèque d'utilitaires" interne, j'ai ajouté un en-tête d'aide pour implémenter ce modèle commun (std::unique_ptr d'un type incomplet, connu seulement de certains TU pour par exemple éviter de longs temps de compilation ou pour fournir juste une poignée opaque pour les clients).

Il fournit l'échafaudage commun pour ce modèle: une classe deleter personnalisée qui invoque une fonction deleter définie en externe, un alias de type pour a unique_ptravec cette classe deleter et une macro pour déclarer la fonction deleter dans un TU qui a une définition complète de la type. Je pense que cela a une utilité générale, alors voici:

#ifndef CZU_UNIQUE_OPAQUE_HPP
#define CZU_UNIQUE_OPAQUE_HPP
#include <memory>

/**
    Helper to define a `std::unique_ptr` that works just with a forward
    declaration

    The "regular" `std::unique_ptr<T>` requires the full definition of `T` to be
    available, as it has to emit calls to `delete` in every TU that may use it.

    A workaround to this problem is to have a `std::unique_ptr` with a custom
    deleter, which is defined in a TU that knows the full definition of `T`.

    This header standardizes and generalizes this trick. The usage is quite
    simple:

    - everywhere you would have used `std::unique_ptr<T>`, use
      `czu::unique_opaque<T>`; it will work just fine with `T` being a forward
      declaration;
    - in a TU that knows the full definition of `T`, at top level invoke the
      macro `CZU_DEFINE_OPAQUE_DELETER`; it will define the custom deleter used
      by `czu::unique_opaque<T>`
*/

namespace czu {
template<typename T>
struct opaque_deleter {
    void operator()(T *it) {
        void opaque_deleter_hook(T *);
        opaque_deleter_hook(it);
    }
};

template<typename T>
using unique_opaque = std::unique_ptr<T, opaque_deleter<T>>;
}

/// Call at top level in a C++ file to enable type %T to be used in an %unique_opaque<T>
#define CZU_DEFINE_OPAQUE_DELETER(T) namespace czu { void opaque_deleter_hook(T *it) { delete it; } }

#endif
Matteo Italia
la source
1

Ce n'est peut-être pas la meilleure solution, mais parfois vous pouvez utiliser shared_ptr à la place. Bien sûr, c'est un peu exagéré, mais ... comme pour unique_ptr, j'attendrai peut-être 10 ans de plus jusqu'à ce que les fabricants de normes C ++ décident d'utiliser lambda comme suppresseur.

Un autre côté. Selon votre code, il peut arriver que lors de la destruction, window_impl soit incomplet. Cela pourrait être une raison de comportement indéfini. Voir ceci: Pourquoi, vraiment, la suppression d'un type incomplet est un comportement indéfini?

Donc, si possible, je définirais un objet très basique pour tous vos objets, avec destructeur virtuel. Et tu es presque bon. Vous devez simplement garder à l'esprit que le système appellera un destructeur virtuel pour votre pointeur, vous devez donc le définir pour chaque ancêtre. Vous devez également définir la classe de base dans la section héritage comme virtuelle (voir ceci pour plus de détails).

Stepan Dyatkovskiy
la source