J'utilise l'idiome de pimpl 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, je reçois 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
'
Autant que je sache, std::unique_ptr
devrait pouvoir être utilisé avec un type incomplet. Est-ce un bug dans libc ++ ou est-ce que je fais quelque chose de mal ici?
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
};
sinon, le compilateur en génère un par défaut et nécessite une déclaration complète de foo::impl
pour cela.
Si vous avez des constructeurs de templates, alors vous êtes foutu, même si vous n'avez pas construit le membre impl_
template <typename T>
foo::foo(T bar)
{
// Here the compiler needs to know how to
// destroy impl_ in case an exception is
// thrown !
}
Avec la portée de l'espace de noms, utiliser 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 à durée statique. Une solution de contournement est:
class impl;
struct ptr_impl : std::unique_ptr<impl>
{
~ptr_impl(); // Implement (empty body) elsewhere
} impl_;
Comme mentionné par Alexandre C. , le problème revient au destructeur de window
qui est défini implicitement aux endroits où le type de window_impl
est encore incomplet. En plus de ses solutions, une autre solution 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;
}
utilise un deleter 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 sa fonction membre unique_ptr::reset()
(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 cela a déjà été souligné dans une autre réponse, une solution consiste à déplacer les toutes opérations nécessitant unique_ptr::~unique_ptr()
, unique_ptr::operator=(unique_ptr&&)
et unique_ptr::reset()
dans le fichier source dans lequel la classe d'assistance pimpl est réellement définie.
Cependant, ceci est plutôt gênant et défie, dans une certaine mesure, le but même de la pimpl. Une solution beaucoup plus propre qui évite tout ce qui consiste à utiliser un deleter personnalisé et à ne déplacer que sa définition dans le fichier source où réside la classe d’aide boutons. 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 séparée, vous pouvez également utiliser une fonction libre ou un membre static
de foo
conjointement avec un lambda:
class foo {
struct pimpl;
static void delete_pimpl(pimpl*);
std::unique_ptr<pimpl,[](pimpl*ptr){delete_pimpl(ptr);}> m_pimpl;
};
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 window doivent être dans un fichier .cpp. Et pour window_impl aussi ...
Au fait, vous devez explicitement ajouter une déclaration de destructeur pour la classe windows dans votre fichier .h.
Mais vous ne POUVEZ PAS mettre de corps de répertoire vide dans votre fichier d’en-tête:
class window {
virtual ~window() {};
}
Doit être juste une déclaration:
class window {
virtual ~window();
}
Ce n’est peut-être pas la meilleure solution, mais vous pouvez parfois utiliser shared_ptr à la place de . Si, bien sûr, c’est un peu exagéré, mais ... comme pour unique_ptr, j’attendrai peut-être 10 ans de plus jusqu’à la définition des standards C++ décidera 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 du comportement indéfini . Voir ceci: Pourquoi, vraiment, supprimer 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. N'oubliez pas que le système appelle 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 en tant que virtuelle (voir this pour plus de détails).
Pour ajouter aux réponses de l'autre concernant le suppresseur personnalisé, dans notre "bibliothèque d'utilitaires" interne, j'ai ajouté un en-tête d'assistance pour implémenter ce modèle commun (std::unique_ptr
d'un type incomplet, connu uniquement par certains des TU pour éviter par exemple les temps de compilation ou pour fournir simplement un traitement opaque aux clients).
Il fournit l’échafaudage commun pour ce modèle: une classe deleter personnalisée qui appelle une fonction deleter définie en externe, un alias de type pour un unique_ptr
avec cette classe deleter et une macro pour déclarer la fonction deleter dans un TU qui a une définition complète du type. Je pense que cela a une utilité générale, alors le 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