使用智能指针实现返回类型协变

37

在 C++ 中我们可以这样做:

struct Base
{
   virtual Base* Clone() const { ... }
   virtual ~Base(){}
};

struct Derived : Base
{
   virtual Derived* Clone() const {...} //overrides Base::Clone
};

然而,以下代码无法实现相同的效果:

struct Base
{
   virtual shared_ptr<Base> Clone() const { ... }
   virtual ~Base(){}
};

struct Derived : Base
{
   virtual shared_ptr<Derived> Clone() const {...} //hides Base::Clone
};
在这个例子中,Derived::Clone 隐藏了 Base::Clone,而不是像覆盖一样重写它,因为标准规定重写的成员函数只能从返回基类的引用(或指针)变成返回派生类的引用(或指针)。有没有聪明的解决方法?当然可以说Clone函数应该始终返回普通指针,但现在先忘记这个 - 这只是一个说明性的例子。我正在寻找一种方法,使虚函数的返回类型从智能指针指向Base变成智能指针指向Derived

提前感谢!

更新:我的第二个例子确实无法编译,感谢Iammilind


2
@Kerrek 可能是因为 Clone() 也可能在非多态上下文中被调用,而在这种情况下不希望丢失类型信息。 - Karl Knechtel
1
正确的名称是协变返回类型,这可能会在标题上有所改进。 - David Rodríguez - dribeas
可能是重复的问题:如何在智能指针中使用协变返回类型? - IntrepidCuriosity
为什么C++委员会总是解决一个问题,然后又一次又一次地引入另一个麻烦呢? - camino
4个回答

52

直接进行操作是不可能的,但有几种模拟方法可以借助于非虚函数接口技巧来实现。

对裸指针使用协变性,然后对其进行封装

struct Base
{
private:
   virtual Base* doClone() const { ... }

public:
   shared_ptr<Base> Clone() const { return shared_ptr<Base>(doClone()); }

   virtual ~Base(){}
};

struct Derived : Base
{
private:
   virtual Derived* doClone() const { ... }

public:
   shared_ptr<Derived> Clone() const { return shared_ptr<Derived>(doClone()); }
};

只有当您实际拥有原始指针时,此方法才有效。

通过转换模拟协方差

struct Base
{
private:
   virtual shared_ptr<Base> doClone() const { ... }

public:
   shared_ptr<Base> Clone() const { return doClone(); }

   virtual ~Base(){}
};

struct Derived : Base
{
private:
   virtual shared_ptr<Base> doClone() const { ... }

public:
   shared_ptr<Derived> Clone() const
      { return static_pointer_cast<Derived>(doClone()); }
};

在这里,您必须确保所有 Derived::doClone 的覆盖实际上返回指向 Derived 或从其派生的类的指针。


2
+1 这是我会推荐的解决方案。(实际上,我会首先建议不要使用 shared_ptr。对于一个函数返回 shared_ptr 很少是一个好策略。但是同样的解决方案也适用于 auto_ptr 或其他智能指针。) - James Kanze
1
我和@James Kanze 的观点一致:在接口中使用特定的共享指针会强制用户选择智能指针。考虑使用不同类型的智能指针(如unique_ptrauto_ptr?)或原始指针来为用户打开选择的机会。(unique_ptrauto_ptr的优点是用户可以获取该指针的所有权并将其传递给不同类型的智能指针,而对于shared_ptr,用户无法放弃所有权) - David Rodríguez - dribeas
很多情况取决于指针来自哪里,以及为什么要返回它。如果它是一个新构造的对象,并且调用者需要接管它的责任,则unique_ptrauto_ptr是最佳解决方案。如果责任在被调用者身上,或者对象已经存在并且正在被管理(可能是由它自己管理),则应使用原始指针。 - James Kanze
3
这仍然是C++需要解决的最大问题之一。在任何地方使用shared_ptr确实非常有用,但这仍然是一个很大的麻烦。 - Glenn Maynard
@quant_dev:好的。 - Stefan Monov
显示剩余4条评论

3
在这个例子中,Derived::Clone 隐藏了 Base::Clone 而不是覆盖它。
不,它并没有隐藏它。实际上,这是一个编译错误
您不能使用另一个函数来覆盖或隐藏虚函数,而该函数仅在返回类型上有所不同;因此,返回类型应相同或协变,否则程序是非法的,因此会出现错误。
因此,这意味着没有其他方法可以将 shared_ptr<D> 转换为 shared_ptr<B>。唯一的方法是拥有 B*D* 的关系(您已经在问题中排除了这种情况)。

是的,你说得对。这是一个错误。对于错误的问题我感到抱歉 :) - Armen Tsirunyan
1
语句“返回类型必须相同”是错误的。C++允许协变指针/引用返回类型(协变意味着当您在一个继承层次结构中向下移动时,覆盖的返回类型必须是相同的或者是其自身继承层次结构中的一个对象)。 - David Rodríguez - dribeas

3

使用CRTP技术,@ymett对一个很好的答案进行了改进。这样你就不必担心忘记在派生类中添加非虚函数。

struct Base
{
private:
   virtual Base* doClone() const { ... }

public:
   shared_ptr<Base> Clone() const { return shared_ptr<Base>(doClone()); }

   virtual ~Base(){}
};

template<class T>
struct CRTP_Base : Base
{
public:
   shared_ptr<T> Clone() const { return shared_ptr<T>(doClone()); }
};

struct Derived : public CRTP_Base<Derived>
{
private:
   virtual Derived* doClone() const { ... }
};

0
我脑海中有一些想法。首先,如果你能做出第一个版本,就把那个Clone隐藏起来,再写一个受保护的_clone,实际上返回派生指针。两个Clone都可以利用它。
这引出了为什么要这样做的问题。另一种方法可能是一个强制(外部)函数,在其中你接收一个shared_ptr<Base>,如果可能的话,可以将其强制转换为shared_ptr<Derived>。也许可以按照以下方式进行:
template <typename B, typename D>
shared_ptr<D> coerce(shared_ptr<B>& sb) throw (cannot_coerce)
{
// ...
}

你是指像 static_pointer_cast 或者 dynamic_pointer_cast 这样的东西吗?至于异常规格说明,不用加。 - Puppy
@DeadMG,是的。我一开始没有记住这些函数,但它们与之类似。至于异常处理,好的,只是一个通过中间步骤处理类型差异的想法。 - Diego Sevilla

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