我该如何在智能指针中使用协变返回类型?

80

我有如下代码:

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 {...}
     ...
};

这段代码不能编译。

在Visual Studio中会引发以下错误:

  

C2555:覆盖的虚函数的返回类型不同且不是协变的

如果我不使用boost::shared_ptr而是返回裸指针,那么代码可以编译(我理解这是由于C++中的协变返回类型)。我可以看到问题是因为Ret1boost::shared_ptr没有派生自RetInterfaceboost::shared_ptr。但我想要返回Ret1boost::shared_ptr以供其他类使用,否则我必须在返回后进行类型转换。

  1. 我做错了什么吗?
  2. 如果没有,为什么语言会像这样 - 它应该可以扩展以处理智能指针之间的转换?有没有可取的解决方法?

如果您不使用boost::shared_ptr,那么您会返回指针吗?这是托管C++吗? - Lev
@Lev 如果我尝试返回原始指针,代码会编译通过,但是会出现内存管理问题。不,我没有使用托管的C++。 - amit kumar
我的做法是:返回原始指针,但要记录调用者负责将指针包装在智能指针中,例如 std::unique_ptr<Class>(obj.clone()) - quant_dev
我可以看出问题是因为Ret1的boost::shared_ptr没有从RetInterface的boost::shared_ptr派生。 不,它不是。 - curiousguy
由于智能指针在这里无法使用,在这种情况下建议使用 gsl::owner<A1*>(请参阅CppCoreguidelines中的C-130建议),以进行深拷贝。 - user6547518
7个回答

33
首先,在C++中,派生类中的虚函数的返回类型必须与基类中的返回类型相同。有一个特殊规定,如果一个函数返回一个指向某个类X的引用/指针,则可以通过返回一个指向从X派生的类的引用/指针来重载该函数。但正如您所指出的,这不能用于智能指针(例如shared_ptr),只适用于普通指针。
如果您的接口RetInterface足够全面,那么在调用代码中,您不需要知道实际返回类型。总体上来说,这并没有意义:之所以get_r是一个虚函数,是因为您将通过指向基类AInterface的指针或引用调用它,在这种情况下,您无法知道派生类将返回什么类型。如果您使用实际的A1引用调用此函数,则可以在A1中创建一个单独的get_r1函数来执行所需操作。
class A1: public AInterface
{
  public:
     boost::shared_ptr<RetInterface> get_r() const
     {
         return get_r1();
     }
     boost::shared_ptr<Ret1> get_r1() const {...}
     ...
};

或者,您可以使用访问者模式或类似于我Dynamic Double Dispatch技术的方法将回调传递到返回的对象中,然后该回调可以使用正确的类型调用回调函数。


boost::shared_ptr<Ret1> 会自动转换为 boost::shared_ptr<RetInterface> 吗?难道你不需要至少使用 boost::static_pointer_cast 这样的东西吗? - h9uest
7
在实现多个接口的类上使用克隆方法,返回指向派生类的指针是有意义的。 - quant_dev

6

这篇博客文章中(来自Raoul Borges),有一个巧妙的解决方案。

在添加对多重继承和抽象方法支持之前,先摘录一小段:

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 = c->clone();
 
   cloneable * p = c.get();
   std::unique_ptr<clonable> pp = p->clone();
}

我鼓励大家阅读全文。文章简单易懂且解释清晰。


不是b->clone(); 而是c->clone()。 - user6547518
fixed as stated. Thanks. - Bruce Adams

2
在C++中重载方法时,对于非指针、非引用类型的返回值类型是无法更改的。 A1::get_r 必须返回一个 boost::shared_ptr<RetInterface>
Anthony Williams有一个很好的综合性 答案

1
这个解决方案怎么样:
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);
}
};

e.g:

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_; }
};

1
这是我的尝试:

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;
};

一个小例子:

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();
}

解释:

这个解决方案涉及元编程和修改协变智能指针中使用的类。

简单的模板结构体 Child 用于绑定类型 Parent 和继承关系。任何从 Child<T> 继承的类都将从 T 继承并将 T 定义为 Parent。协变智能指针中使用的类需要定义此类型。

has_parent 用于在编译时检测一个类是否定义了类型 Parent。这部分不是我的,我使用了与检测方法是否存在相同的代码 (请看这里)

由于我们希望智能指针支持协变,我们希望我们的智能指针模仿现有的类架构。在示例中更容易解释它的工作原理。

当定义一个 CovariantSharedPtr<B> 时,它继承自 ICovariantSharedPtr<B>,这被解释为 ICovariantSharedPtr<B, has_parent<B>::value>。由于 B 继承自 Child<A>,所以 has_parent<B>::value 为 true,因此 ICovariantSharedPtr<B>ICovariantSharedPtr<B, true> 并继承自 ICovariantSharedPtr<B::Parent>,即 ICovariantSharedPtr<A>。由于 A 没有定义 Parenthas_parent<A>::value 为 false,ICovariantSharedPtr<A>ICovariantSharedPtr<A, false> 并且不继承任何内容。

重点在于BA继承,因此我们有ICovariantSharedPtr<B>ICovariantSharedPtr<A>继承。因此,任何返回ICovariantSharedPtr<A>指针或引用的方法都可以被返回ICovariantSharedPtr<B>的相同方法重载。


对于那些没有太多时间来解释这个的人,加入一些基本原理和解释怎么样?它与morabot相比如何?谢谢。 - Quartz

-1

Mr Fooz回答了你问题的第一部分。对于第二部分,它之所以这样工作是因为编译器在编译时不知道它将调用AInterface::get_r还是A1::get_r - 它需要知道它将得到什么返回值,因此它坚持要求两种方法返回相同的类型。这是C++规范的一部分。

对于解决方法,如果A1::get_r返回指向RetInterface的指针,则RetInterface中的虚拟方法仍将按预期工作,并且当指针被销毁时将删除正确的对象。没有必要使用不同的返回类型。


-2
也许你可以使用一个输出参数来解决“与返回的boost shared_ptrs协变”的问题。
 void get_r_to(boost::shared_ptr<RetInterface>& ) ...

因为我怀疑调用者可能会传递一个更精细的shared_ptr类型作为参数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接