【问题标题】:Passing shared pointers as arguments将共享指针作为参数传递
【发布时间】:2012-06-05 07:02:47
【问题描述】:

如果我声明一个包含在共享指针中的对象:

std::shared_ptr<myClass> myClassObject(new myClass());

然后我想将它作为参数传递给方法:

DoSomething(myClassObject);

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

以上是否只是增加了 shared_pt 的引用计数并且一切都很酷?还是留下一个悬空指针?

你还应该这样做吗?:

DoSomething(myClassObject.Get());

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

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

谢谢。

【问题讨论】:

  • const std::shared_ptr&lt;myClass&gt;&amp; arg1
  • 第二种方式被破坏了,如果你真的需要你的函数来共享所有权,第一种方式是惯用的。但是,DoSomething 真的需要共享所有权吗?看起来它应该只是参考...
  • @SteveH :它没有,但是如果你的函数实际上不需要它们,为什么要在它的调用者上强制使用特殊的对象所有权语义呢?您的函数没有比void DoSomething(myClass&amp; arg1) 更好的功能。
  • @SteveH :智能指针的全部目的是处理普通的对象所有权问题——如果你没有这些,你不应该首先使用智能指针。 ;-] 具体到shared_ptr&lt;&gt;,您必须传递值才能实际共享所有权。
  • 无关:一般来说,std::make_shared 不仅效率更高,而且saferstd::shared_ptr 构造函数还要好。

标签: c++ c++11 shared-ptr c++-faq


【解决方案1】:

我想将共享指针传递给函数。你能帮我解决这个问题吗?

当然,我可以帮你。我假设您对 C++ 中的所有权语义有一定的了解。这是真的吗?

是的,我对这个主题相当满意。

很好。

好吧,我只能想到两个理由来接受shared_ptr 参数:

  1. 函数想要共享对象的所有权;
  2. 该函数执行一些专门针对shared_ptrs 的操作。

你对哪一个感兴趣?

我正在寻找一个普遍的答案,所以我实际上对两者都感兴趣。不过,我很好奇你在案例 2 中的意思。

此类函数的示例包括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 左值引用,另一个采用右值引用。一个复制,另一个移动。完美转发功能模板是另一种选择。

我认为这涵盖了所有可能的情况。非常感谢。

【讨论】:

  • @Jon:为什么?这就是我应该做A还是应该做B。我想我们都知道如何通过值/引用传递对象,不是吗?
  • 因为像“这是否意味着我将通过对 const 的引用传递它来制作副本?不,这是一种悲观”这样的散文会让初学者感到困惑。传递对 const 的引用不会复制。
  • @Martinho 我不同意“如果你想操纵指针,就拿指针”的规则。如this 回答中所述,不获取对象的临时所有权可能很危险。在我看来,默认应该是传递一个临时所有权的副本,以保证函数调用期间对象的有效性。
  • @radman 您链接到的答案中没有任何场景适用该规则,因此它完全不相关。请给我写一个 SSCCE,您在其中通过引用从 shared_ptr&lt;T&gt; ptr;(即 void f(T&amp;),用 f(*ptr) 调用)传递,并且该对象不会超过调用。或者写一个你通过值void f(T) 并遇到麻烦的地方。
  • @Martinho 查看我的example code 以获得简单的自包含正确示例。该示例适用于void f(T&amp;) 并涉及线程。我认为我的问题是您的回答语气不鼓励传递 shared_ptr 的所有权,这是使用它们的最安全、最直观和最简单的方式。我非常反对建议初学者提取对 shared_ptr 拥有的数据的引用,滥用的可能性很大,而且好处很小。
【解决方案2】:

我认为人们不必要地害怕使用原始指针作为函数参数。如果函数不打算存储指针或以其他方式影响其生命周期,则原始指针也可以正常工作并代表最低公分母。例如,考虑如何将 unique_ptr 传递给以 shared_ptr 作为参数的函数,无论是通过值还是通过 const 引用?

void DoSomething(myClass * p);

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

作为函数参数的原始指针不会阻止您在调用代码中使用智能指针,这很重要。

【讨论】:

  • 如果你使用的是指针,为什么不直接使用引用呢? DoSomething(*a_smart_ptr)
  • @Xeo,你是对的——那会更好。不过,有时您需要考虑 NULL 指针的可能性。
  • 如果您习惯使用原始指针作为输出参数,则在调用代码时更容易发现变量可能会更改,例如:比较 fun(&amp;x)fun(x)。在后一个示例中,x 可以按值传递,也可以作为 const ref 传递。如上所述,它还允许您传递nullptr如果您对输出不感兴趣...
  • @AndreasMagnusson:在处理从另一个源传入的指针时,这种将指针作为输出参数的约定可能会给人一种错误的安全感。因为在这种情况下调用是 fun(x) 并且 *x 被修改并且源没有 &x 来警告你。
  • 如果函数调用超出调用者的范围怎么办?有可能是共享ptr的析构函数被调用了,现在该函数将尝试访问已删除的内存,导致UB
【解决方案3】:

是的,关于 shared_ptr 的整个想法是多个实例可以保存相同的原始指针,并且只有在 shared_ptr 的最后一个实例被销毁时才会释放底层内存。

我会避免指向 shared_ptr 的指针,因为这会破坏您现在再次处理 raw_pointers 的目的。

【讨论】:

    【解决方案4】:

    在您的第一个示例中按值传递是安全的,但有一个更好的习惯用法。尽可能通过 const 引用传递 - 即使在处理智能指针时我也会说是。您的第二个示例并没有完全损坏,但它非常 !???。愚蠢的,什么都没做,并且破坏了智能指针的部分要点,当您尝试取消引用和修改事物时,会让您陷入痛苦的错误世界。

    【讨论】:

    • 没有人提倡通过指针传递,如果你的意思是原始指针。如果没有所有权争用,通过引用传递当然是惯用的。关键是,关于shared_ptr&lt;&gt;,您必须复制一份才能真正分享所有权;如果您有指向 shared_ptr&lt;&gt; 的引用或指针,那么您不会共享任何内容,并且会遇到与普通引用或指针相同的生命周期问题。
    • @ildjarn:在这种情况下传递一个常量引用是可以的。调用者的原始shared_ptr 保证比函数调用更持久,因此该函数在使用shared_ptr&lt;&gt; const &amp; 时是安全的。如果它需要存储指针以供以后使用,则需要进行复制(此时必须进行复制,而不是持有引用),但不需要产生复制成本除非 你需要这样做。在这种情况下,我会去传递一个常量引用....
    • @David : "在这种情况下传递一个常量引用是可以的。" 在任何健全的 API 中都没有——为什么 API 会要求一个甚至不是智能指针类型利用而不是仅仅采用普通的 const 引用?这几乎与缺乏 const 正确性一样糟糕。如果你的观点是从技术上讲它不会伤害任何东西,那么我当然同意,但我认为这不是正确的做法。
    • ...另一方面,如果该函数不需要延长对象的生命周期,那么您只需从接口中删除shared_ptr并传递一个普通的(const ) 对指向对象的引用。
    • @David :如果您的 API 使用者一开始没有使用 shared_ptr&lt;&gt; 怎么办?现在,您的 API 真的很让人头疼,因为它迫使调用者无意义地更改其对象生命周期语义以使用它。我看不出有什么值得提倡的,除了说“它在技术上没有伤害任何东西”。我没有看到任何关于“如果您不需要共享所有权,请不要使用 shared_ptr&lt;&gt;”的争议。
    【解决方案5】:

    在你的函数中 DoSomething 您正在更改 myClass 类实例的数据成员 所以你正在修改的是托管(原始指针)对象而不是(shared_ptr)对象。这意味着在此函数的返回点,所有指向托管原始指针的共享指针都将看到它们的数据成员:myClass::someField 更改为不同的值。

    在这种情况下,您将一个对象传递给一个函数,并保证您不会修改它(谈论 shared_ptr 而不是拥有的对象)。

    表达这一点的成语是:a const ref, like so

    void DoSomething(const std::shared_ptr&lt;myClass&gt;&amp; arg)

    同样,您向函数的用户保证,您不会将另一个所有者添加到原始指针的所有者列表中。但是,您保留了修改原始指针指向的基础对象的可能性。

    CAVEAT:这意味着,如果有人在你调用你的函数之前调用了shared_ptr::reset,并且在那一刻它是最后一个拥有raw_ptr的shared_ptr,那么你的对象将被销毁,你的函数将操纵一个悬空指向被破坏对象的指针。非常危险!!!

    【讨论】:

      猜你喜欢
      • 2014-01-21
      • 2012-01-24
      • 1970-01-01
      • 2018-05-14
      • 2020-11-08
      • 2018-06-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多