将shared_ptr<Derived>作为shared_ptr<Base>传递

143

如何将派生类型的shared_ptr传递给接受基础类型shared_ptr的函数,这是最佳方法?

通常我通过引用传递shared_ptr以避免不必要的复制:

int foo(const shared_ptr<bar>& ptr);

但是如果我尝试做类似这样的事情,这种方法就不起作用了

int foo(const shared_ptr<Base>& ptr);

...

shared_ptr<Derived> bar = make_shared<Derived>();
foo(bar);

我可以使用

foo(dynamic_pointer_cast<Base, Derived>(bar));

但是这种方法有两个缺点:

  • dynamic_cast似乎对于一个简单的派生类到基类转换来说有点过度。
  • 据我所知,dynamic_pointer_cast创建了一个指针的副本(尽管是临时的),以传递给函数。

有更好的解决方案吗?

更新备忘录:

结果发现这是一个缺失头文件的问题。而且,我在这里尝试做的事情被认为是反模式。通常情况下:

  • 不会影响对象生存期的函数(即在函数执行期间该对象保持有效)应采用普通引用或指针,例如int foo(bar& b)

  • 消耗对象的函数(即给定对象的最终用户)应采用按值传递的unique_ptr,例如int foo(unique_ptr<bar> b)。调用者应使用std::move将值移动到函数中。

  • 扩展对象生存期的函数应采用按值传递的shared_ptr,例如int foo(shared_ptr<bar> b)。避免循环引用的通常建议同样适用。

详见Herb Sutter的基础回顾讲座


9
为什么要传递一个 shared_ptr?为什么不能传递 bar 的 const 引用? - ipc
2
任何dynamic转换只需要用于向下转换。此外,传递派生指针应该完全可以工作。它将创建一个具有相同引用计数(并增加它)和指向基类的指针的新shared_ptr,然后绑定到const引用。但是,既然您已经使用了引用,我不明白为什么您要使用shared_ptr。取一个Base const&并调用foo(*bar)即可。 - Xeo
@Xeo:传递派生指针(即foo(bar))在MSVC 2010中不起作用。 - Matt Kline
1
“明显不起作用”是什么意思?代码编译并正确执行,您是在问如何避免创建临时的 shared_ptr 以传递给函数吗?我非常确定这是无法避免的。 - Mike Seymour
1
@Seth:我不同意。我认为有理由按值传递共享指针,而几乎没有理由按引用传递共享指针(这一切都不需要提倡不必要的复制)。在这里推理https://dev59.com/PGgv5IYBdhLWcg3wI9Wg#10826907 - R. Martinho Fernandes
显示剩余8条评论
4个回答

83

如果您忘记在派生类上指定public继承,也会发生这种情况,例如像我一样写成:

class Derived : Base
{
};

改为:

class Derived : public Base
{
};

1
class 用于模板参数;struct 用于定义类。(这最多只是个玩笑。) - Davis Herring
3
这肯定应该被视为解决方案,不需要进行强制类型转换,只需添加 public 即可。 - Alexis Paques
救了我!非常感谢。我想知道为什么强制转换只适用于公共继承,您能否详细说明一下? - Pavel Nasevich

60

尽管 BaseDerived 是协变的,对它们的裸指针将相应地起作用,但 shared_ptr<Base>shared_ptr<Derived> 不是协变的。使用 dynamic_pointer_cast 是解决这个问题的正确且最简单的方法。

(编辑: 根据下面的评论,static_pointer_cast 更合适,因为你从派生类向基类进行转换,这是安全的,不需要运行时检查。)

然而,如果你的 foo() 函数不希望参与延长对象寿命(或者说不希望参与共享所有权),那么最好接受一个 const Base& 并在传递给 foo() 时取消引用 shared_ptr

void foo(const Base& base);
[...]
shared_ptr<Derived> spDerived = getDerived();
foo(*spDerived);

另外,由于shared_ptr类型不能协变,返回shared_ptr<T>类型时不适用于跨协变返回类型的隐式转换规则。


49
它们不是协变的,但 shared_ptr<Derived> 可以隐式转换为 shared_ptr<Base>,因此代码应该可以运行而无需进行任何强制转换操作。 - Mike Seymour
11
shared_ptr<Ty>有一个构造函数,它接受一个shared_ptr<Other>参数,并在Ty*隐式可转换为Other*时进行适当的转换。如果需要转换,static_pointer_cast是适当的转换方式,而不是dynamic_pointer_cast - Pete Becker
真的,但不是使用他在问题中提到的引用参数。无论如何,他都需要进行复制。但是,如果他正在使用shared_ptr的引用来避免引用计数,那么首先使用shared_ptr没有什么好的理由。最好使用const Base&代替。 - Bret Kuhns
@KimHomann 在我回答的例子中,引用计数不会增加。通过引用传递指向的对象(例如 const Foobar&)不会导致 std::shared_ptr 影响计数。然而,使用 static_pointer_cast(或 dynamic_point_cast)将影响引用计数。后者必须这样做,因为它参与了对象的生命周期。 - Bret Kuhns
1
@TanveerBadar 不确定。也许这在2012年无法编译?(尤其是使用Visual Studio 2010或2012)。但您绝对是正确的,如果完整的/公开派生/类定义对编译器可见,则OP的代码应该可以编译。 - Bret Kuhns
显示剩余5条评论

20

还要检查包含派生类完整声明的头文件的#include是否在您的源代码文件中。

我遇到了这个问题。 std::shared<derived> 无法转换为 std::shared<base>。 我提前声明了两个类,以便可以持有指向它们的指针,但由于没有#include,编译器无法看到一个类是从另一个类派生而来的。


3
哇,我没想到这个方法居然解决了我的问题。我一直很小心地只在需要的地方包含头文件,所以有些头文件只在源文件中,像你说的,在头文件中前向声明它们。 - jigglypuff
3
愚蠢的编译器真是太愚蠢了。这就是我的问题所在。谢谢! - Tanveer Badar

16

听起来你可能过于费力了。 shared_ptr 的复制成本很低;这是它的目标之一。通过引用传递它们并没有什么实际作用。如果你不想共享,可以传递原始指针。

话虽如此,我能够想到两种方法来达成这个目标:

foo(shared_ptr<Base>(bar));
foo(static_pointer_cast<Base>(bar));

11
不,它们不便宜复制,应尽可能通过引用传递。 - Seth Carnegie
7
赛斯·卡内基问:赫伯特是否对你的代码进行了分析,看看按值传递是否成为瓶颈? - Pete Becker
32
@SethCarnegie - 那并没有回答我所问的问题。而且,值得一提的是,我编写了 Microsoft 发布的 shared_ptr 实现。 - Pete Becker
8
@SethCarnegie - 你把启发式方法搞反了。手动优化通常应该不要进行,除非你能证明它们是必需的。 - Pete Becker
22
只有在你需要付出努力时,才会被认为是“过早”的优化。我认为,在使用高效的语言习惯而非低效的语言习惯方面,无论在特定情境下是否有所区别,都没有问题。 - Mark Ransom
显示剩余11条评论

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