将由函数返回的共享指针绑定到左值引用常量上是一个好的实践吗?

4

虽然我花了一点时间来适应,但我现在已经养成了一个习惯,即让我的函数通过左值引用传递共享指针参数到const,而不是按值传递(当然,如果我需要修改原始参数,我会通过左值引用传递非const参数):

void foo(std::shared_ptr<widget> const& pWidget)
//                               ^^^^^^
{
    // work with pWidget...
}

这样做的好处是避免了不必要的共享指针副本,这意味着线程安全地增加引用计数并可能产生不必要的开销。

现在我一直在思考是否采用一种对于从函数中以值返回的共享指针具有相对称习惯的方法,例如在以下代码片段的末尾检索:

struct X
{
    // ...
    std::shared_ptr<Widget> bar() const
    {
        // ...
        return pWidget;
    }
    // ...
    std::shared_ptr<Widget> pWidget;
};

// ...

// X x;
std::share_ptr<Widget> const& pWidget = x.bar();
//                     ^^^^^^

采用这种编码习惯有什么注意事项吗?一般情况下,将返回的共享指针分配给另一个共享指针对象而非绑定到引用上是否更好呢?


假设该函数正在为 Widget 分配内存,那么在其结束后,将不会有任何共享的内容。因此,从一开始就返回 shared_ptr 没有太多意义。使用 unique_ptr 更为适当,是吧? - Joseph Mansfield
@sftrabbit:好的。在我的具体用例中,bar() 最常见的是一个成员函数,它返回一个数据成员 shared_ptr。已经编辑问题以反映这一点。 - Andy Prowl
相反,如果在您的设计中知道该对象最终将由不同的对象共享,则首先使用make_shared创建shared_ptr可以通过减少成本(一个 vs. 两个内存分配)来帮助。 - David Rodríguez - dribeas
3个回答

6

这只是一个关于是否捕获对临时变量的const引用比创建副本更有效的旧问题的翻版。简单的答案是不是。在以下这行代码中:

// std::shared_ptr<Widget> bar();
std::shared_ptr<Widget> const & pWidget = bar();

编译器需要创建一个本地的未命名变量(不是临时变量),用bar()的调用来初始化它,然后将引用绑定到它上面。
std::shared_ptr<Widget> __tmp = bar();
std::shared_ptr<Widget> const & pWidget = __tmp;

在大多数情况下,它将避免创建引用并仅在函数的其余部分中使用原始对象的别名,但归根结底,无论变量是称为pWidget还是__tmp并进行别名设置都不会带来任何优势。
相反,对于普通读者来说,bar()看起来可能并没有创建对象,而是产生了对已存在的std::shared_ptr<Widget>的引用,因此维护者将不得不查找bar()的定义,以了解pWidget是否可以在该函数范围之外更改。
通过绑定到const引用的生命周期扩展是语言中一个很奇怪的功能,几乎没有什么实际用途(即当引用是基类并且您不太关心由value返回的确切派生类型时,即ScopedGuard)。

@user1131467:没错。当将代码翻译成编译器解释的代码时,我通常使用指针而不是引用,并且语法就在其中蠕动。已修复,谢谢。 - David Rodríguez - dribeas

3
您可能把优化反了:
struct X
{
    // ...
    std::shared_ptr<Widget> const& bar() const
    {
        // ...
        return pWidget;
    }
    // ...
    std::shared_ptr<Widget> pWidget;
};

// ...

// X x;
std::share_ptr<Widget>  pWidget = x.bar();

由于您的版本中 bar 返回一个成员变量,因此必须复制 shared_ptr。如果您通过引用返回成员变量,则可以避免复制。

在您的原始版本和上面显示的版本中都无所谓,但如果调用了以下内容,则会出现问题:

x.bar()->baz()

在你的版本中,会创建一个新的shared_ptr,然后调用baz。

在我的版本中,baz直接在成员复制的shared_ptr上调用,并避免了原子引用的增加/减少。

当然,shared_ptr复制构造函数(原子增量)的成本非常小,在除了最注重性能的应用程序之外甚至不值得注意。如果你正在编写非常注重性能的应用程序,则更好的选择是使用内存池架构手动管理内存,然后谨慎地使用原始指针。


2
David Rodríguez-dribeas所说的内容上补充一点,即绑定到一个const引用并不能避免复制,而且计数器仍然会增加。以下代码说明了这一点:
#include <memory>
#include <cassert>

struct X {
    std::shared_ptr<int> p;
    X() : p{new int} {}
    std::shared_ptr<int> bar() { return p; }
};

int main() {
    X x;
    assert(x.p.use_count() == 1);
    std::shared_ptr<int> const & p = x.bar();
    assert(x.p.use_count() == 2);
    return 0;
}

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