c++ covariance smart-pointers

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.

  1. ¿Estoy haciendo algo mal?
  2. 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.