c++ - ¿Cómo puedo usar tipos de retorno covariantes con punteros inteligentes?
covariance smart-pointers (6)
Tengo un código como este:
class RetInterface {...}
class Ret1: public RetInterface {...}
class AInterface
{
public:
virtual boost::shared_ptr<RetInterface> get_r() const = 0;
...
};
class A1: public AInterface
{
public:
boost::shared_ptr<Ret1> get_r() const {...}
...
};
Este código no se compila.
En el estudio visual aumenta
C2555: el tipo de retorno de función virtual anulable difiere y no es covariante
Si no uso boost::shared_ptr
pero devuelvo punteros sin procesar, el código se compila (entiendo que esto se debe a los tipos de retorno covariantes en C ++). Puedo ver que el problema es porque boost::shared_ptr
de Ret1
no se deriva de boost::shared_ptr
de RetInterface
. Pero quiero devolver boost::shared_ptr
de Ret1
para usar en otras clases, de lo contrario, debo convertir el valor devuelto después de la devolución.
- ¿Estoy haciendo algo mal?
- De lo contrario, ¿por qué el lenguaje es así? ¿Debería ser extensible para manejar la conversión entre punteros inteligentes en este escenario? ¿Hay una solución deseable?
En primer lugar, así es como funciona en C ++: el tipo de retorno de una función virtual en una clase derivada debe ser el mismo que en la clase base. Existe la excepción especial de que una función que devuelve una referencia / puntero a alguna clase X puede ser anulada por una función que devuelve una referencia / puntero a una clase que se deriva de X, pero como se nota, esto no permite punteros inteligentes. (como shared_ptr
), solo para los indicadores simples.
Si su interfaz RetInterface
es suficientemente completa, entonces no necesitará saber el tipo real devuelto en el código de llamada. En general, no tiene sentido de todos modos: la razón por la cual get_r
es una función virtual
, en primer lugar, se debe a que la AInterface
través de un puntero o referencia a la clase base AInterface
, en cuyo caso no puedes saber de qué tipo clase derivada volvería. Si está llamando a esto con una referencia A1
real, puede simplemente crear una función get_r1
separada en A1
que haga lo que necesita.
class A1: public AInterface
{
public:
boost::shared_ptr<RetInterface> get_r() const
{
return get_r1();
}
boost::shared_ptr<Ret1> get_r1() const {...}
...
};
Alternativamente, puede usar el patrón de visitante o algo así como mi técnica de Despacho dinámico doble para pasar una devolución de llamada al objeto devuelto que puede invocar la devolución de llamada con el tipo correcto.
tal vez podrías usar un parámetro de salida para evitar la "covarianza con el boost devuelto shared_ptrs".
void get_r_to(boost::shared_ptr<RetInterface>& ) ...
ya que sospecho que una persona que llama puede utilizar un tipo de argumento shared_ptr más refinado.
¿Qué pasa con esta solución?
template<typename Derived, typename Base>
class SharedCovariant : public shared_ptr<Base>
{
public:
typedef Base BaseOf;
SharedCovariant(shared_ptr<Base> & container) :
shared_ptr<Base>(container)
{
}
shared_ptr<Derived> operator ->()
{
return boost::dynamic_pointer_cast<Derived>(*this);
}
};
p.ej:
struct A {};
struct B : A {};
struct Test
{
shared_ptr<A> get() {return a_; }
shared_ptr<A> a_;
};
typedef SharedCovariant<B,A> SharedBFromA;
struct TestDerived : Test
{
SharedBFromA get() { return a_; }
};
Aquí está mi intento:
template<class T>
class Child : public T
{
public:
typedef T Parent;
};
template<typename _T>
class has_parent
{
private:
typedef char One;
typedef struct { char array[2]; } Two;
template<typename _C>
static One test(typename _C::Parent *);
template<typename _C>
static Two test(...);
public:
enum { value = (sizeof(test<_T>(nullptr)) == sizeof(One)) };
};
class A
{
public :
virtual void print() = 0;
};
class B : public Child<A>
{
public:
void print() override
{
printf("toto /n");
}
};
template<class T, bool hasParent = has_parent<T>::value>
class ICovariantSharedPtr;
template<class T>
class ICovariantSharedPtr<T, true> : public ICovariantSharedPtr<typename T::Parent>
{
public:
T * get() override = 0;
};
template<class T>
class ICovariantSharedPtr<T, false>
{
public:
virtual T * get() = 0;
};
template<class T>
class CovariantSharedPtr : public ICovariantSharedPtr<T>
{
public:
CovariantSharedPtr(){}
CovariantSharedPtr(std::shared_ptr<T> a_ptr) : m_ptr(std::move(a_ptr)){}
T * get() final
{
return m_ptr.get();
}
private:
std::shared_ptr<T> m_ptr;
};
Y un pequeño ejemplo:
class UseA
{
public:
virtual ICovariantSharedPtr<A> & GetPtr() = 0;
};
class UseB : public UseA
{
public:
CovariantSharedPtr<B> & GetPtr() final
{
return m_ptrB;
}
private:
CovariantSharedPtr<B> m_ptrB = std::make_shared<B>();
};
int _tmain(int argc, _TCHAR* argv[])
{
UseB b;
UseA & a = b;
a.GetPtr().get()->print();
}
Explicaciones
Esta solución implica metaprogramación y modificar las clases utilizadas en punteros inteligentes covariantes.
La plantilla simple struct Child
está aquí para enlazar el tipo Parent
y inheritance. Cualquier clase que herede de Child<T>
heredará de T
y definirá T
como principal. Las clases utilizadas en punteros inteligentes covariantes necesitan este tipo para definirse.
La clase has_parent
se usa para detectar en tiempo de compilación si una clase define el tipo Parent
o no. Esta parte no es mía, utilicé el mismo código para detectar si existe un método ( ver aquí )
Como queremos la covarianza con punteros inteligentes, queremos que nuestros punteros inteligentes imiten la arquitectura de clase existente. Es más fácil explicar cómo funciona en el ejemplo.
Cuando se define CovariantSharedPtr<B>
, hereda de ICovariantSharedPtr<B>
, que se interpreta como ICovariantSharedPtr<B, has_parent<B>::value>
. Como B
hereda de Child<A>
, has_parent<B>::value
es verdadero, entonces ICovariantSharedPtr<B>
es ICovariantSharedPtr<B, true>
y hereda de ICovariantSharedPtr<B::Parent>
que es ICovariantSharedPtr<A>
. Como A
no tiene ninguno definido, has_parent<A>::value
es falso, ICovariantSharedPtr<A>
es ICovariantSharedPtr<A, false>
y hereda de nada.
El punto principal es que B
hereda de A
, tenemos ICovariantSharedPtr<B>
hereda de ICovariantSharedPtr<A>
. Por lo tanto, cualquier método que devuelva un puntero o una referencia en ICovariantSharedPtr<A>
puede sobrecargarse mediante un método que devuelva el mismo en ICovariantSharedPtr<B>
.
No puede cambiar los tipos de devolución (para los tipos de devolución no puntero ni de referencia) al sobrecargar métodos en C ++. A1::get_r
debe devolver un boost::shared_ptr<RetInterface>
.
Anthony Williams tiene una buena respuesta integral.
El Sr. Fooz respondió la primera parte de su pregunta. Parte 2, funciona de esta manera porque el compilador no sabe si llamará a AInterface :: get_r o A1 :: get_r en tiempo de compilación; necesita saber qué valor de retorno va a obtener, por lo que insiste en ambos métodos devolviendo el mismo tipo. Esto es parte de la especificación C ++.
Para la solución alternativa, si A1 :: get_r devuelve un puntero a RetInterface, los métodos virtuales en RetInterface seguirán funcionando como se esperaba, y el objeto adecuado se eliminará cuando se destruya el puntero. No hay necesidad de diferentes tipos de devolución.