Como posso usar tipos de retorno covariant com ponteiros inteligentes?
-
10-07-2019 - |
Pergunta
Eu tenho um 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 não compila.
No visual studio levanta ??p>
C2555: substituindo tipo diferente de retorno de função virtual e não é covariant
Se eu não usar boost::shared_ptr
mas retornam ponteiros crus, as compilações de código (eu entendo que isso é devido a tipos de retorno covariantes em C ++). Eu posso ver o problema é porque boost::shared_ptr
de Ret1
não é derivado de boost::shared_ptr
de RetInterface
. Mas eu quero voltar boost::shared_ptr
de Ret1
para uso em outras classes, senão eu deve converter o valor devolvido após o retorno.
- Estou fazendo algo errado?
- Se não, por que é a linguagem como este - ele deve ser extensível para lidar com a conversão entre ponteiros inteligentes neste cenário? Existe uma solução desejável?
Solução
Em primeiro lugar, esta é realmente como ele funciona em C ++: o tipo de retorno de uma função virtual em uma classe derivada deve ser o mesmo que na classe base. Não é a exceção especial que uma função que retorna uma referência / ponteiro para alguma classe X pode ser substituído por uma função que retorna uma referência / ponteiro para uma classe que deriva de X, mas como você note que este não permite inteligente ponteiros (como shared_ptr
), apenas para ponteiros simples.
Se o seu RetInterface
interface é suficientemente abrangente, então você não precisa saber o tipo retornado real no código de chamada. Em geral, não faz sentido qualquer maneira: o get_r
razão é uma função virtual
em primeiro lugar é porque você estará chamando-o através de um ponteiro ou referência ao AInterface
classe base, caso em que você não pode saber o que digite o classe derivada voltaria. Se você está chamando isso com uma referência A1
real, você pode simplesmente criar uma função get_r1
separado no A1
que faz o que você precisa.
class A1: public AInterface
{
public:
boost::shared_ptr<RetInterface> get_r() const
{
return get_r1();
}
boost::shared_ptr<Ret1> get_r1() const {...}
...
};
Como alternativa, você pode usar o padrão do visitante ou algo como técnica meu dupla dinâmica Despacho para passar uma chamada de retorno para o objeto retornado que pode invocar o callback com o tipo correto.
Outras dicas
E sobre esta solução:
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);
}
};
por exemplo:
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_; }
};
Aqui é a minha tentativa:
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;
};
E um pequeno exemplo:
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();
}
Explicações:
Esta solução implica meta-programação e modificar as classes usadas no ponteiros inteligentes covariantes.
O simples Child
template struct está aqui para vincular o Parent
tipo e herança. Qualquer classe herdada de Child<T>
vai herdar de T
e definir T
como Parent
. As classes utilizadas na ponteiros inteligentes covariantes precisa deste tipo a ser definido.
O has_parent
classe é usada para detectar em tempo de compilação se uma classe define o tipo Parent
ou não. Esta parte não é minha, eu usei o mesmo código para detectar se existe um método ( veja aqui )
Como queremos covariância com ponteiros inteligentes, queremos que nossos ponteiros inteligentes para imitar a arquitetura classe existente. É mais fácil explicar como ele funciona no exemplo.
Quando um CovariantSharedPtr<B>
é definido, ele herda de ICovariantSharedPtr<B>
, que é interpretado como ICovariantSharedPtr<B, has_parent<B>::value>
. Como herda B
de Child<A>
, has_parent<B>::value
é verdade, então ICovariantSharedPtr<B>
é ICovariantSharedPtr<B, true>
e herda de ICovariantSharedPtr<B::Parent>
que é ICovariantSharedPtr<A>
. Como A
não tem Parent
definido, has_parent<A>::value
é falso, ICovariantSharedPtr<A>
é ICovariantSharedPtr<A, false>
e herda a partir do nada.
O ponto principal é como B
inherits de A
, temos ICovariantSharedPtr<B>
inheriting de ICovariantSharedPtr<A>
. Assim, qualquer método retornando um ponteiro ou uma referência em ICovariantSharedPtr<A>
pode ser sobrecarregado por um método retornar o mesmo em ICovariantSharedPtr<B>
.
Existe uma solução elegante postou em este post (de Raoul Borges)
Um trecho do pouco antes de adicionar suporte para herança mulitple e métodos abstratos é:
template <typename Derived, typename Base>
class clone_inherit<Derived, Base> : public Base
{
public:
std::unique_ptr<Derived> clone() const
{
return std::unique_ptr<Derived>(static_cast<Derived *>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const override
{
return new Derived(*this);
}
};
class concrete: public clone_inherit<concrete, cloneable>
{
};
int main()
{
std::unique_ptr<concrete> c = std::make_unique<concrete>();
std::unique_ptr<concrete> cc = b->clone();
cloneable * p = c.get();
std::unique_ptr<clonable> pp = p->clone();
}
Gostaria de incentivar a leitura do artigo completo. Seu simplesmente escrito e bem explicado.
Sr. Fooz respondeu a parte 1 da sua pergunta. Parte 2, ele funciona dessa maneira porque o compilador não sei se ele vai estar chamando AInterface :: get_r ou A1 :: get_r em tempo de compilação - ele precisa saber o valor de retorno que vai para chegar, por isso insiste em ambos os métodos devolver o mesmo tipo. Esta é parte da especificação C ++.
Para a solução alternativa, se A1 :: get_r retorna um ponteiro para RetInterface, os métodos virtuais em RetInterface irá ainda trabalho como esperado, e o próprio objeto será excluído quando o ponteiro é destruído. Não há necessidade de diferentes tipos de retorno.
talvez você poderia usar um parâmetro de saída para contornar "covariância com devolvidos shared_ptrs impulso.
void get_r_to(boost::shared_ptr<RetInterface>& ) ...
desde que eu suspeito que um chamador pode cair em um tipo de shared_ptr mais refinado como argumento.