传递共享指针作为参数

108
如果我声明了一个被 shared pointer 包装的对象:
std::shared_ptr<myClass> myClassObject(new myClass());

然后我想把它作为一个参数传递给一个方法:

DoSomething(myClassObject);

//the called method
void DoSomething(std::shared_ptr<myClass> arg1)
{
   arg1->someField = 4;
}

以上代码是简单地增加了shared_ptr的引用计数,一切都很好吗?还是它留下了悬空指针?

你还应该这样做吗?

DoSomething(myClassObject.Get());

void DoSomething(std::shared_ptr<myClass>* arg1)
{
   (*arg1)->someField = 4;
}

我认为第二种方法可能更有效,因为它只需要复制1个地址(而不是整个智能指针),但第一种方法似乎更易读,并且我不预计会推动性能极限。我只想确保这样做没有什么危险。

谢谢。


16
const std::shared_ptr<myClass>& arg1 可以翻译为“参数 arg1 是一个指向 myClass 对象的 shared_ptr,同时是一个常量引用”。 - Captain Obvlious
3
第二种方式已经失效,如果你确实需要函数共享所有权,则第一种方法是惯用的。但是,“DoSomething”函数真的需要共享所有权吗?看起来它应该只需要使用引用即可... - ildjarn
8
@SteveH:虽然如此,但如果您的函数实际上并不需要特殊的对象所有权语义,为什么要强制其调用者这样做呢?您的函数没有任何必要使用 void DoSomething(myClass& arg1) 更好的功能。 - ildjarn
2
@SteveH:智能指针的整个目的是处理非常规对象所有权问题——如果您没有这些问题,首先就不应该使用智能指针。;-] 至于特定的 shared_ptr<>,您必须按值传递才能实际共享所有权。 - ildjarn
4
一般来说,std::make_shared不仅更高效,而且比std::shared_ptr构造函数更安全 - R. Martinho Fernandes
显示剩余6条评论
5个回答

195
我想把一个共享指针传递给一个函数。你能帮我吗?
当然,我可以帮你。我假设你对C++中的所有权语义有一些了解,是这样吗?
是的,我对这个主题还算熟悉。
很好。
好的,我只能想到两个原因来接受一个shared_ptr参数:
1. 函数想要共享对象的所有权; 2. 函数执行一些特定于shared_ptr的操作。
你对哪个感兴趣?
我在寻找一个通用答案,所以实际上我对两个都感兴趣。不过,我对第二种情况有点好奇。
这样的函数示例包括std::static_pointer_cast、自定义比较器或谓词。例如,如果你需要从向量中找到所有唯一的shared_ptr,你就需要这样一个谓词。
啊,当函数实际上需要操作智能指针本身时。
确切地说。
在那种情况下,我认为我们应该通过引用传递。
是的。如果不改变指针,你可以通过const引用传递。因为不需要共享所有权,所以不需要复制。这是另一种情况。
好的,明白了。我们来谈谈另一种情况。
那种情况下你要共享所有权?好的。你如何使用shared_ptr来共享所有权?
通过复制它。
那么函数需要复制一个shared_ptr,对吗?
显然。所以我通过const引用传递,并复制到一个局部变量中?
不,那样会降低性能。如果通过引用传递,函数只能手动进行复制。如果通过值传递,编译器会自动选择最佳方式(复制或移动)。所以,传递值。
好主意。我必须经常记住那篇 "Want Speed? Pass by Value." 文章。
等一下,如果函数将 shared_ptr 存储在数据成员中,会不会造成冗余的拷贝?
函数可以简单地将 shared_ptr 参数移动到其存储中。移动 shared_ptr 是廉价的,因为它不会更改任何引用计数。
啊,好主意。
但是我在考虑第三种情况:如果你不想操纵 shared_ptr,也不想共享所有权怎么办?
在这种情况下,shared_ptr 对于函数来说完全无关紧要。如果你想操纵指针对象,请接受一个指针对象,并让调用者选择他们想要的所有权语义。
我应该通过引用还是值来接受指针对象?
通常的规则适用。智能指针不会改变任何东西。
如果我要复制,就按值传递;如果我想避免复制,就按引用传递。
没错。
嗯,我觉得你忘记了另一种情况。如果我只想在特定条件下共享所有权怎么办?
啊,一个有趣的边界情况。我不指望这种情况经常发生。但当它发生时,你可以选择按值传递并忽略复制(如果你不需要它),或者按引用传递并进行复制(如果你需要它)。
第一种选择会多出一个多余的复制,而第二种选择会失去一个潜在的移动。难道我不能两全其美吗?
如果你处于真正关键的情况下,你可以提供两个重载函数,一个接受const左值引用,另一个接受右值引用。一个复制,另一个移动。另外,完美转发的函数模板也是一个选择。
我觉得这样涵盖了所有可能的情况。非常感谢你。

2
@Jon:为什么?这完全是关于我应该做_A_还是_B_。我想我们都知道如何按值/引用传递对象,不是吗? - sbi
9
像“这是否意味着我通过对常量的引用来传递它就能使副本?不,那是一种性能下降”的散文对于初学者来说很令人困惑。通过对常量的引用传递不会制作副本。 - Jon
1
@Martinho,我不同意“如果你想操作指针,请使用指针”的规则。正如这个答案中所述,不获取对象的临时所有权可能是危险的。在我看来,默认情况下应该传递一个副本以进行临时所有权,以保证函数调用期间对象的有效性。 - radman
当您需要将派生类型作为基本类型传递时怎么办?这需要使用指针,对吗? - daaxix
没事了,只要阅读参考资料就可以避免切片问题。 - daaxix
显示剩余9条评论

32

我认为人们不必要地害怕在函数参数中使用裸指针。如果函数不会存储指针或以其他方式影响其生命周期,那么裸指针同样有效,并代表了最低公共分母。例如考虑如何将unique_ptr传递给以值或常量引用方式接受shared_ptr参数的函数。

void DoSomething(myClass * p);

DoSomething(myClass_shared_ptr.get());
DoSomething(myClass_unique_ptr.get());

一个作为函数参数的裸指针并不会防止你在调用代码中使用智能指针,那里才是真正重要的地方。


9
如果你正在使用指针,为什么不直接使用引用呢?DoSomething(*a_smart_ptr) - Xeo
6
@Xeo,你是正确的 - 那将是更好的。但有时需要考虑到指针为空的可能性。 - Mark Ransom
1
如果您有使用原始指针作为输出参数的惯例,那么在调用代码中更容易发现变量可能会改变,例如:将fun(&x)fun(x)进行比较。在后一个示例中,x可以通过值传递或作为const引用传递。正如上面所述,它还允许您传递nullptr,如果您对输出不感兴趣... - Andreas Magnusson
2
@AndreasMagnusson:当处理从其他来源传递的指针时,指针作为输出参数的约定可能会给人一种虚假的安全感。因为在这种情况下,调用是fun(x),*x被修改,而源没有&x来警告你。 - Zan Lynx
如果该函数在单独的线程中被调用,它的行为可能会发生改变,不是吗? - Sridhar Thiagarajan
显示剩余4条评论

5

是的,shared_ptr<> 的整个概念就是多个实例可以持有相同的原始指针,只有当最后一个 shared_ptr<> 实例被销毁时,底层内存才会被释放。

我建议避免使用指向 shared_ptr<> 的指针,因为这样做会破坏其目的,您现在又要处理原始指针了。


2

在你的第一个例子中,按值传递是安全的,但有更好的习惯用法。尽可能使用const引用传递 - 即使处理智能指针也是如此。你的第二个例子并不完全有问题,但它非常愚蠢,没有完成任何任务,并且破坏了智能指针的部分目的,当你尝试解除引用和修改内容时,会让你陷入错误的痛苦世界。


3
如果你指的是裸指针,那么没有人主张传递裸指针。如果没有所有权争议,通过引用传递参数是很常见的。重点是,对于特定的shared_ptr<>,你必须进行复制才能实现共享所有权;如果你有一个shared_ptr<>的引用或指针,那么你并没有共享任何东西,并且会受到与普通引用或指针相同的生命周期问题的影响。 - ildjarn
2
@ildjarn:在这种情况下传递一个常量引用是可以的。调用者的原始shared_ptr保证会比函数调用更长久,因此函数在使用shared_ptr<> const &时是安全的。如果需要在以后的某个时间存储指针,则需要复制(此时必须进行复制,而不是持有引用),但是除非需要这样做,否则没有必要承担复制的成本。在这种情况下,我会选择传递一个常量引用... - David Rodríguez - dribeas
3
“在这种情况下传递一个常量引用是可以的。”但在任何合理的API中都不是如此-为什么API会强制使用智能指针类型而不是只采用普通的常量引用?这几乎和缺少常量正确性一样糟糕。如果你的观点是技术上它并没有造成伤害,那我当然同意,但我认为这并不是正确的做法。 - ildjarn
2
另一方面,如果函数不需要延长对象的生命周期,则可以从接口中删除shared_ptr并传递指向对象的普通(const)引用。 - David Rodríguez - dribeas
3
假设你的API消费者一开始没有使用shared_ptr<>,现在你的API变得非常棘手,因为它迫使调用者无谓地改变自己的对象生命周期语义才能使用它。我并不认为这有什么值得倡导的地方,除了说“从技术上讲它没有造成任何伤害”。如果你不需要共享所有权,我认为“不要使用shared_ptr<>”这个观点并没有什么争议。 - ildjarn
显示剩余11条评论

1
在您的函数DoSomething中,您正在更改类myClass的实例的数据成员,因此您修改的是托管的(原始指针)对象,而不是(shared_ptr)对象。这意味着在此函数的返回点上,所有指向托管原始指针的共享指针都将看到其数据成员:myClass :: someField更改为不同的值。
在这种情况下,您正在将一个对象传递给一个函数,并保证不会对其进行修改(谈论shared_ptr而不是owned object)。
表达这个习惯用法的方式是通过:一个const引用,如下所示 void DoSomething(const std::shared_ptr<myClass>& arg) 同样,您向函数的用户保证,您不会将另一个所有者添加到原始指针的所有者列表中。但是,您保留了修改指向原始指针的基础对象的可能性。

注意:这意味着,如果在您调用函数之前某人以某种方式调用了shared_ptr::reset,并且此时它是最后一个拥有原始指针的 shared_ptr,则您的对象将被销毁,并且您的函数将操作指向已销毁对象的悬空指针。非常危险!


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