c++ - example - make_unique
El método de sobrecarga para unique_ptr y shared_ptr es ambiguo con el polimorfismo (5)
Codificando cosas después de tomar el consejo de la respuesta de mi pregunta anterior , me encontré con un problema con la sobrecarga de Scene :: addObject.
Para reiterar los bits relevantes y hacer esto autónomo, con el menor detalle posible:
- Tengo una jerarquía de objetos heredados de la
Interface
de los cuales hayFoo
s yBar
s; - Tengo una
Scene
que posee estos objetos; -
Foo
s debe serunique_ptr
s yBar
s debe sershared_ptr
s en mi principal (por las razones explicadas en la pregunta anterior); - El
main
pasa a la instancia deScene
, que toma posesión.
Ejemplo de código mínimo es 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);
}
Descomentar los resultados de las líneas comentadas en:
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)
^~~~~
Descomentar lo compartido y comentar las cosas únicas que también se compilan, así que supongo que el problema está, como dice el compilador, en la sobrecarga. Sin embargo, necesito la sobrecarga, ya que estos dos tipos deberán almacenarse en algún tipo de colección y, de hecho, se mantendrán como punteros a la base (posiblemente todos se shared_ptr
movido a shared_ptr
s).
Estoy pasando ambos valores por valor porque quiero aclarar que estoy tomando posesión de Scene
(y estoy subiendo el contador de referencia para shared_ptr
s). No me queda muy claro cuál es el problema, y no pude encontrar ningún ejemplo de esto en ningún otro lugar.
Los Foos deben ser unique_ptrs y las Barras deben ser shared_ptrs en mi principal (por las razones explicadas en la pregunta anterior);
¿Puede sobrecargar en términos de puntero a Foo
y puntero a Bar
lugar de puntero a Interface
, ya que desea tratarlos de manera diferente?
El problema con el que se encuentra es que este constructor de shared_ptr
(13) , (que no es explícito), es una coincidencia tan buena como un constructor similar de "mover derivado a base" de unique_ptr
(6) (tampoco es explícito).
template< class Y, class Deleter >
shared_ptr( std::unique_ptr<Y,Deleter>&& r ); // (13)
13) Construye un
shared_ptr
que administra el objeto actualmente administrado porr
. El marcador asociado conr
se almacena para la eliminación futura del objeto gestionado.r
gestiona ningún objeto después de la llamada.Esta sobrecarga no participa en la resolución de sobrecarga si
std::unique_ptr<Y, Deleter>::pointer
no es compatible conT*
. Sir.get()
es un puntero nulo, esta sobrecarga es equivalente al constructor predeterminado (1). (desde C ++ 17)
template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u ) noexcept; //(6)
6) Construye un
unique_ptr
transfiriendo la propiedad deu
a*this
, dondeu
se construye con un deleter especificado (E).Este constructor solo participa en la resolución de sobrecarga si todo lo siguiente es verdadero:
a)
unique_ptr<U, E>::pointer
es convertible implícitamente a punterob)
U
no es un tipo de matrizc) O bien
Deleter
es un tipo de referencia yE
es el mismo tipo queD
, oDeleter
no es un tipo de referencia yE
es convertible implícitamente aD
En el caso no polimórfico, estás construyendo un unique_ptr<T>
partir de un unique_ptr<T>&&
, que utiliza el constructor de movimiento sin plantilla. Hay resolución de sobrecarga que prefiere la no-plantilla.
Voy a suponer que Scene
almacena shared_ptr<Interface>
s. En ese caso, no necesita sobrecargar addObject
para unique_ptr
, solo puede permitir la conversión implícita en la llamada.
Ha declarado dos sobrecargas, una tomando std::unique_ptr<Interface>
y una tomando std::shared_ptr<Interface>
pero está pasando un parámetro de tipo std::unique_ptr<Foo>
. Como ninguna de sus funciones coincide directamente, el compilador debe realizar una conversión para llamar a su función.
Hay una conversión disponible para std::unique_ptr<Interface>
(conversión de tipo simple a puntero único a clase base) y otra a std::shared_ptr<Interface>
(cambiar a un puntero compartido a la clase base). Estas conversiones tienen la misma prioridad, por lo que el compilador no sabe qué conversión usar, por lo que sus funciones son ambiguas.
Si pasa std::unique_ptr<Interface>
o std::shared_ptr<Interface>
no se requiere conversión, por lo que no hay ambigüedad.
La solución es simplemente eliminar la sobrecarga de unique_ptr
y convertir siempre a shared_ptr
. Esto supone que las dos sobrecargas tienen el mismo comportamiento, si no cambian el nombre de uno de los métodos podría ser más apropiado.
La otra respuesta explica la ambigüedad y una posible solución. Aquí hay otra manera en caso de que termine necesitando ambas sobrecargas; siempre puede agregar otro parámetro en tales casos para romper la ambigüedad y usar el envío de etiquetas. El código de la placa de la caldera está oculto en la parte privada de la 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{});
}
};
El ejemplo compilable completo está here.
La solución de jrok ya es bastante buena. El siguiente enfoque permite reutilizar el código aún mejor:
#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
}
Lo que hacemos aquí es introducir primero algunas clases de ayuda que permiten volver a enlazar los punteros inteligentes.