243 votes

std::unique_ptr avec un type incomplet ne compilera pas

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 invalide de ' sizeof ' à un type incomplet ' uixx::window::window_impl '

Pour autant que je sache, std::unique_ptr devrait pouvoir être utilisé avec un type incomplet. S'agit-il d'un bogue dans libc++ ou est-ce que je fais quelque chose de mal ici ?

1 votes

Lien de référence pour les exigences de complétude : stackoverflow.com/a/6089065/576911

1 votes

Un pimpl est souvent construit et non modifié depuis. J'utilise habituellement un std::shared_ptr<const window_impl>

0 votes

Relié : J'aimerais beaucoup savoir pourquoi cela fonctionne dans MSVC, et comment l'empêcher de fonctionner (afin de ne pas casser les compilations de mes collègues de GCC).

316voto

Alexandre C. Points 31758

Voici quelques exemples de std::unique_ptr avec des types 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 ça.

Si vous avez des constructeurs de modèles, alors vous êtes fichu, même si vous ne construisez pas l'élément 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 nom, en utilisant unique_ptr ne fonctionnera pas non plus :

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

puisque le compilateur doit savoir ici comment détruire cet objet static duration. Une solution de contournement existe :

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;

7 votes

Je trouve votre première solution (ajouter le foo destructeur) permet à la déclaration de la classe elle-même de compiler, mais la déclaration d'un objet de ce type n'importe où entraîne l'erreur originale ("invalid application of 'sizeof'...").

0 votes

@user192737 : Pouvez-vous s'il vous plaît poster un exemple complet quelque part reproduisant l'erreur (pastebin ou ideone ou autre) ?

1 votes

Bien sûr ! C'est juste votre premier exemple, avec un main() dans lequel la classe est instanciée : pastebin.com/65jMYzsi J'ai découvert par la suite que l'ajout d'un constructeur par défaut à foo fait disparaître l'erreur - je ne sais pas pourquoi.

63voto

Comme Alexandre C. mentionné, le problème se résume à window étant implicitement défini dans des endroits où le type de l'objet de l'utilisateur est différent de celui de l'objet de l'utilisateur. window_impl est encore incomplète. En plus de ses solutions, une autre solution de contournement que j'ai utilisée est de 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 de suppression personnalisée exclut l'utilisation de l'option std::make_unique (disponible à partir de C++14), comme déjà discuté ici .

6 votes

C'est la solution correcte à mon avis. 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 suppresseur par défaut utilisé par std::unique_ptr<X> tente de faire "delete X", ce qu'il ne peut pas faire si X est une déclaration forward. En spécifiant une fonction de suppression, vous pouvez placer cette fonction dans un fichier source où la classe X est complètement définie. D'autres fichiers sources peuvent alors utiliser std::unique_ptr<X, DeleterFunc> même si X n'est qu'une déclaration forward tant qu'ils sont liés avec le fichier source contenant DeleterFunc.

1 votes

C'est une bonne solution 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 fait référence au constructeur et au destructeur), et que vous ne voulez pas les déplacer dans un fichier d'implémentation comme le suggère @adspx5.

0 votes

La classe de suppression peut être la seule solution appropriée dans certains cas. Personnellement, j'utilise une classe make_unique en utilisant une classe de suppression : template<typename _Tp, typename _Deleter, typename... _Args> auto make_unique_with_deleter(_Args&&... __args) { return std::unique_ptr<_Tp, _Deleter>(new _Tp(std::forward<_Args>(__args)...), _Deleter{}); }

29voto

Walter Points 7554

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 mouvement, et unique_ptr::reset() fonction membre (uniquement). Cependant, elles doivent être appelées (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 déjà souligné dans une autre réponse, une façon d'éviter cela est de déplacer tous les opérations qui nécessitent unique_ptr::~unique_ptr() , unique_ptr::operator=(unique_ptr&&) et unique_ptr::reset() dans le fichier source où la classe d'aide pimpl est réellement définie.

Cependant, cela est plutôt gênant et défie dans une certaine mesure l'objectif même du pimpl idoim. Une solution beaucoup plus propre, qui évite tout cela, consiste à utiliser un fichier suppresseur personnalisé et de ne déplacer sa définition que dans le fichier source où se trouve la classe d'aide du Bourgeon. 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 de suppresseur séparée, vous pouvez également utiliser une fonction libre ou static membre de foo en conjonction avec un lambda :

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

17voto

adspx5 Points 176

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

Assurez-vous que dans votre fichier .h pour la classe window, vous n'avez que des déclarations de fonctions. Tous les corps de fonction pour window doivent être dans le fichier .cpp. Et pour window_impl aussi...

En fait, vous devez ajouter explicitement la déclaration du destructeur pour la classe Windows dans votre fichier .h.

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

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

Ce doit être une simple déclaration :

  class window {
    virtual ~window();
  }

0 votes

C'était aussi ma solution. Beaucoup plus concise. Il suffit que votre constructeur/destructeur soit déclaré dans l'en-tête et défini dans le fichier cpp.

3voto

Matteo Italia Points 53117

Pour compléter les réponses des autres concernant le suppresseur personnalisé, j'ai ajouté dans notre "bibliothèque d'utilitaires" interne un en-tête d'aide pour mettre en œuvre ce modèle commun ( std::unique_ptr d'un type incomplet, connu seulement de certaines des UT pour, par exemple, éviter des temps de compilation trop longs ou pour fournir juste un handle opaque aux clients).

Il fournit l'échafaudage commun pour ce modèle : une classe de suppresseur personnalisée qui invoque une fonction de suppresseur définie en externe, un alias de type pour un unique_ptr avec cette classe de suppresseur, et une macro pour déclarer la fonction de suppresseur dans une TU qui a une définition complète du type. Je pense que cela peut avoir une utilité générale, alors la 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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X