web-dev-qa-db-fra.com

Pimpl - Pourquoi make_unique peut-il être appelé sur un type incomplet

Pourquoi l'appel make_unique se compile-t-il? Make_unqiue ne requiert-il pas que son argument de modèle soit un type complet?

struct F;
int main()
{
  std::make_unique<F>();
}

struct F {};

La question d'origine mon "problème" avec ma implémentation PIMPL:

Je comprends pourquoi le destructeur doit être déclaré et défini par l'utilisateur dans le fichier cpp pour la classe d'implémentation (PIMPL).

Mais déplacer le constructeur de la classe contenant le pimpl-still se compile.

class Object
{};

class CachedObjectFactory
{
public:
  CachedObjectFactory();

  ~CachedObjectFactory();
  std::shared_ptr<Object> create(int id) const;

private:
  struct CacheImpl;
  std::unique_ptr<CacheImpl> pImpl;
};

Maintenant, le fichier cpp:

// constructor with make_unique on incompete type ?
CachedObjectFactory::CachedObjectFactory()
  : pImpl(std::make_unique<CacheImpl>())
{}

struct CachedObjectFactory::CacheImpl
{
  std::map<int, std::shared_ptr<Object>> idToObjects;
};

//deferred destructor
CachedObjectFactory::~CachedObjectFactory() = default;

Quelqu'un pourrait-il expliquer pourquoi cela se compile? Pourquoi y a-t-il une différence entre construction et destruction? Si l'instanciation du destructeur et l'instanciation du default_deleter est un problème pourquoi l'instanciation de make_unique n'est pas un problème?

26
AF_cpp

La raison pour laquelle cela se compile est ici dans [temp.point] ¶8 :

Une spécialisation pour un modèle de fonction, un modèle de fonction membre, ou d'une fonction membre ou un membre de données statiques d'un modèle de classe peut avoir plusieurs points d'instanciations dans une unité de traduction, et en plus des points d'instanciation décrits ci-dessus, pour toute spécialisation de ce type ayant un point d'instanciation dans l'unité de traduction, la fin de l'unité de traduction est également considérée comme un point d'instanciation. Une spécialisation pour un modèle de classe a au plus un point d'instanciation dans une unité de traduction [...] Si deux points d'instanciation différents donnent à une spécialisation de modèle des significations différentes selon la règle d'une définition, le programme est mal formé, aucun diagnostic requis.

Notez la fin de cette citation, comme nous y reviendrons dans la modification ci-dessous, mais pour l'instant ce qui se passe en pratique selon l'extrait de l'OP est que le compilateur utilise le en plus considéré point d'instanciation de make_unique() qui est placée à la fin de l'unité de traduction, de sorte qu'elle aura des définitions qui manquent au point d'utilisation d'origine dans le code. Il est autorisé à le faire conformément à cette clause de la spécification.

Notez que cela ne compile plus:

struct Foo; int main(){ std::make_unique<Foo>(); } struct Foo { ~Foo() = delete; };

Comme dans, le compilateur ne manque pas sur le point d'instanciation, il le reporte uniquement en termes de point dans l'unité de traduction qu'il utilise pour générer du code pour le modèle.


Edit: Enfin, il semble que même si vous avez ces multiples points d'instanciation, cela ne signifie pas que le comportement est défini si la définition est différente entre ces points. Notez la dernière phrase de la citation ci-dessus, selon laquelle cette différence est définie par la une règle de définition. Ceci est tiré directement de mon commentaire à la réponse de @hvd, qui a mis cela en lumière ici: Voir ici dans la règle de définition unique :

Chaque programme doit contenir exactement une définition de chaque fonction ou variable non intégrée qui est utilisée par odr dans ce programme en dehors d'une instruction supprimée; aucun diagnostic requis. La définition peut apparaître explicitement dans le programme, elle peut être trouvée dans la bibliothèque standard ou définie par l'utilisateur, ou ...

Et donc dans le cas de l'OP, il y a évidemment une différence entre les deux points d'instanciation, en ce que, comme l'a noté @hvd lui-même, le premier est de type incomplet, et le second ne l'est pas. En effet, cette différence constitue deux définitions différentes et il ne fait donc aucun doute que ce programme est mal formé.

13

make_unique possède plusieurs points d'instanciation: la fin de l'unité de traduction est également un point d'instanciation. Ce que vous voyez, c'est que le compilateur instancie uniquement make_unique une fois CacheImpl/F est terminé. Les compilateurs sont autorisés à le faire. Votre code est mal formé si vous vous y fiez, et les compilateurs ne sont pas tenus de détecter l'erreur.

14.6.4.1 Point d'instanciation [point temp.]

8 Une spécialisation pour un modèle de fonction, un modèle de fonction membre, ou une fonction membre ou un membre de données statiques d'un modèle de classe peut avoir plusieurs points d'instanciations au sein d'une unité de traduction, et en plus des points d'instanciation décrits ci-dessus, pour tout une telle spécialisation qui a un point d'instanciation au sein de l'unité de traduction, la fin de l'unité de traduction est également considérée comme un point d'instanciation. [...] Si deux points d'instanciation différents donnent à une spécialisation de modèle des significations différentes selon la règle de définition unique (3.2), le programme est mal formé, aucun diagnostic requis.

16
user743382