web-dev-qa-db-fra.com

La méthode de surcharge pour unique_ptr et shared_ptr est ambiguë avec le polymorphisme

En codant des choses après avoir pris l’indice de la réponse de ma précédente question , je suis tombé sur un problème de surcharge de Scene :: addObject.

Pour réitérer les bits pertinents et rendre cela autonome, avec le moins de détails possible:

  • J'ai une hiérarchie d'objets héritant de Interface dont il y a Foos et Bars;
  • J'ai un Scene qui possède ces objets;
  • Foos doivent être unique_ptrs et Bars doivent être shared_ptrs dans ma colonne principale (pour les raisons expliquées à la question précédente);
  • la main les passe à l'instance Scene, qui en prend possession.

Exemple de code minimal est this :

#include <memory>
#include <utility>

class Interface
{
public:
  virtual ~Interface() = 0;
};

inline Interface::~Interface() {}

class Foo : public Interface
{
};

class Bar : public Interface
{
};

class Scene
{
public:
  void addObject(std::unique_ptr<Interface> obj);
//  void addObject(std::shared_ptr<Interface> obj);
};

void Scene::addObject(std::unique_ptr<Interface> obj)
{
}

//void Scene::addObject(std::shared_ptr<Interface> obj)
//{
//}

int main(int argc, char** argv)
{
  auto scn = std::make_unique<Scene>();

  auto foo = std::make_unique<Foo>();
  scn->addObject(std::move(foo));

//  auto bar = std::make_shared<Bar>();
//  scn->addObject(bar);
}

Si vous décommentez les lignes commentées, vous obtenez:

error: call of overloaded 'addObject(std::remove_reference<std::unique_ptr<Foo, std::default_delete<Foo> >&>::type)' is ambiguous

   scn->addObject(std::move(foo));

                                ^

main.cpp:27:6: note: candidate: 'void Scene::addObject(std::unique_ptr<Interface>)'

 void Scene::addObject(std::unique_ptr<Interface> obj)

      ^~~~~

main.cpp:31:6: note: candidate: 'void Scene::addObject(std::shared_ptr<Interface>)'

 void Scene::addObject(std::shared_ptr<Interface> obj)

      ^~~~~

Le fait de commenter le contenu partagé et de commenter le contenu unique compile également. Par conséquent, je suppose que le problème réside, comme le dit le compilateur, dans la surcharge. Cependant, j'ai besoin de la surcharge car ces deux types devront être stockés dans une sorte de collection, et ils sont en effet conservés comme des pointeurs vers la base (éventuellement tous déplacés dans shared_ptrs).

Je passe les deux par valeur car je tiens à préciser que je prends possession de Scene (et que le compteur de références pour le shared_ptrs est supérieur). Je ne vois pas très bien où est le problème et je n’ai trouvé aucun exemple ailleurs.

25

Le problème que vous rencontrez est le constructeur de shared_ptr (13) , (ce qui n’est pas explicite), correspond aussi bien qu’un constructeur similaire du type "moving dérived to base" de unique_ptr (6) ( pas non plus explicite). 

template< class Y, class Deleter > 
shared_ptr( std::unique_ptr<Y,Deleter>&& r ); // (13)

13) Construit un shared_ptr qui gère l’objet actuellement géré par r. Le deleter associé à r est stocké pour une suppression ultérieure de l'objet géré. r ne gère aucun objet après l'appel.

Cette surcharge ne participe pas à la résolution de la surcharge si std::unique_ptr<Y, Deleter>::pointer n'est pas compatible avec T*. Si r.get() est un pointeur NULL, cette surcharge est équivalente au constructeur par défaut (1). (depuis C++ 17)

template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u ) noexcept; //(6)

6) Construit un unique_ptr en transférant la propriété de u à *this, où u est construit avec un deleter spécifié (E). 

Ce constructeur ne participe à la résolution de surcharge que si toutes les conditions suivantes sont remplies:

a) unique_ptr<U, E>::pointer est implicitement convertible en pointeur

b) U n'est pas un type de tableau

c) Soit Deleter est un type de référence et E est du même type que D, ou Deleter n'est pas un type de référence et E est implicitement convertible en D

Dans le cas non polymorphe, vous construisez un unique_ptr<T> à partir d'un unique_ptr<T>&&, qui utilise le constructeur de déplacement sans modèle. Il résolution de surcharge préfère le non-modèle


Je vais supposer que Scene enregistre shared_ptr<Interface>s. Dans ce cas, vous n'avez pas besoin de surcharger addObject pour unique_ptr, vous pouvez simplement autoriser la conversion implicite dans l'appel.

18
Caleth

L'autre réponse explique l'ambiguïté et une solution possible. Voici une autre solution au cas où vous auriez besoin des deux surcharges. vous pouvez toujours ajouter un autre paramètre dans de tels cas pour lever l'ambiguïté et utiliser la distribution de balises. Le code de la plaque de la chaudière est caché dans la partie privée de Scene:

class Scene
{
    struct unique_tag {};
    struct shared_tag {};
    template<typename T> struct tag_trait;
    // Partial specializations are allowed in class scope!
    template<typename T, typename D> struct tag_trait<std::unique_ptr<T,D>> { using tag = unique_tag; };
    template<typename T>             struct tag_trait<std::shared_ptr<T>>   { using tag = shared_tag; };

  void addObject_internal(std::unique_ptr<Interface> obj, unique_tag);
  void addObject_internal(std::shared_ptr<Interface> obj, shared_tag);

public:
    template<typename T>
    void addObject(T&& obj)
    {
        addObject_internal(std::forward<T>(obj),
            typename tag_trait<std::remove_reference_t<T>>::tag{});
    }
};

Exemple compilable complet est ici.

4
jrok

Vous avez déclaré deux surcharges, une en prenant std::unique_ptr<Interface> et une en prenant std::shared_ptr<Interface>, mais transmettez un paramètre de type std::unique_ptr<Foo>. Comme aucune de vos fonctions ne correspond directement, le compilateur doit effectuer une conversion pour appeler votre fonction.

Il existe une conversion disponible pour std::unique_ptr<Interface> (conversion de type simple en pointeur unique vers la classe de base) et une autre en std::shared_ptr<Interface> (modification d'un pointeur partagé vers la classe de base). Ces conversions ont la même priorité afin que le compilateur ne sache pas quelle conversion utiliser, vos fonctions sont donc ambiguës.

Si vous passez std::unique_ptr<Interface> ou std::shared_ptr<Interface>, aucune conversion n'est requise, il n'y a donc aucune ambiguïté.

La solution consiste simplement à supprimer la surcharge unique_ptr et à toujours convertir en shared_ptr. Cela suppose que les deux surcharges ont le même comportement. S'ils ne renomment pas l'une des méthodes, cela pourrait être plus approprié.

3
Alan Birtles

La solution de Jrok est déjà assez bonne. L'approche suivante permet de réutiliser le code encore mieux:

#include <memory>
#include <utility>
#include <iostream>
#include <type_traits>

namespace internal {
    template <typename S, typename T>
    struct smart_ptr_rebind_trait {};

    template <typename S, typename T, typename D>
    struct smart_ptr_rebind_trait<S,std::unique_ptr<T,D>> { using rebind_t = std::unique_ptr<S>; };

    template <typename S, typename T>
    struct smart_ptr_rebind_trait<S,std::shared_ptr<T>> { using rebind_t = std::shared_ptr<S>; };

}

template <typename S, typename T>
using rebind_smart_ptr_t = typename internal::smart_ptr_rebind_trait<S,std::remove_reference_t<T>>::rebind_t;

class Interface
{
public:
  virtual ~Interface() = 0;
};

inline Interface::~Interface() {}

class Foo : public Interface {};

class Bar : public Interface {};

class Scene
{
  void addObject_internal(std::unique_ptr<Interface> obj) { std::cout << "unique\n"; }

  void addObject_internal(std::shared_ptr<Interface> obj) { std::cout << "shared\n"; }

public:

  template<typename T>
  void addObject(T&& obj) {
    using S = rebind_smart_ptr_t<Interface,T>;
    addObject_internal( S(std::forward<T>(obj)) );
  }   
};

int main(int argc, char** argv)
{
  auto scn = std::make_unique<Scene>();

  auto foo = std::make_unique<Foo>();
  scn->addObject(std::move(foo));

  auto bar = std::make_shared<Bar>();
  scn->addObject(bar); // ok
}

Ce que nous faisons ici est d’abord introduire quelques classes d’aide permettant de relier les pointeurs intelligents.

1
Handy999

Les foos doivent être unique_ptrs et les barres doivent être shared_ptrs dans mon main (pour les raisons expliquées à la question précédente);

Pouvez-vous surcharger en termes de pointeur-à -Foo et de pointeur-à -Bar au lieu de pointeur-à -Interface, puisque vous souhaitez les traiter différemment?

0
WaffleSouffle