【问题标题】:Pass by value or reference, to a C++ constructor that needs to store a copy?通过值或引用传递给需要存储副本的 C++ 构造函数?
【发布时间】:2023-03-26 07:24:02
【问题描述】:

当 C++(隐式或显式)值构造函数需要在其对象中存储参数的副本时,是否应该通过值或引用到常量来接受其参数?

这是我能想到的最短的例子:

struct foo {
    bar _b;
    foo(bar [const&] b) // pass by value or reference-to-const?
        : _b(b) { }
};

这里的想法是,我想在创建 foo 对象时,以任何可能创建 foo 对象的各种方式,最小化对 bar 的复制构造函数的调用。

请注意,我确实对复制省略和(命名)返回值优化有所了解,并且我已经阅读了"Want Speed? Pass by Value",但我认为这篇文章并没有直接解决这个用例。

编辑:我应该更具体。

假设我不知道sizeof(bar),或者bar 是否是基本的内置类型(bar 可能是模板参数,foo 可能是类模板一类)。另外,不要假设foo 的构造函数可以被内联(或者bar 的,就此而言)。请假设我至少可能正在使用实现 RVO 的编译器。

我希望有一种可能性(考虑到编译器优化)这样的调用不会调用任何对bar 的复制构造函数的调用(即使在foo 中执行_b(b) 时也是如此)初始化列表):

foo f = function_that_creates_and_returns_a_bar_object_using_rvo();

是否有可能(给定 C++98 标准)可以做到这一点,如果是这样,如果foo 通过引用到常量而不是通过价值?

【问题讨论】:

    标签: c++ optimization constructor pass-by-reference pass-by-value


    【解决方案1】:

    在所有条件相同的情况下,对于足够复杂的类,我使用 const&,对于 POD 和简单对象,我使用 value。

    列出通过 const 引用而不是传统的按值传递的优缺点

    正面:

    • 避免复制(对于具有昂贵副本的对象来说是一大优势)
    • 只读访问

    否定:

    • 如果他们真的想的话,有人可以将 const 从引用中转换出来

    更重要的是,您明确控制复制发生的时间(在您的情况下,在初始化列表中传递初始化 _b 时)。考虑负面因素……我同意这是一种风险。我认为几乎所有优秀的程序员都会对使用 const_cast 感到肮脏。此外,您可以尽职尽责地搜索 const_cast 并将西红柿扔向从参数中抛出 const 的人。但是,嘿,你永远不知道,谁有时间像鹰一样看代码:)?

    我的主观意见是,在足够复杂的类和性能很重要的环境中,避免复制构造函数的好处大于风险。但是对于真正愚蠢的 POD 类,我倾向于复制数据并按值传递。

    【讨论】:

      【解决方案2】:

      看看这个question

      如果sizeof(T)>sizeof(void*) 使用const T & arg,如果sizeof(T) <= sizeof(void*) 使用T arg。所有基本类型都应该是这条规则的例外

      【讨论】:

      • 虽然我认为这是一个很好的经验法则,但它似乎与参考文章提出的指导方针相冲突(至少有时)......在我的具体用例中, foo 是一个类带有 bar 作为模板参数的模板,所以我不知道 sizeof(bar) (也许我应该在我的示例中包含它:()。
      • 使用类型特征根据 sizeof(bar) 选择引用或值
      • @Alexey:除了函子和迭代器(根据 Meyers),它们应该总是按值传递,然后我们希望编译器做正确的事情。不过,不确定迈耶斯抽了什么烟来得出这个结论。
      • @Alexey:关于类型特征的要点。我可以看到类型特征在哪里可以提供类似 value_or_const_ref::type 的东西,如果 bar 是内置类型,或者如果 sizeof(bar)
      【解决方案3】:

      在 C++98 和 C++03 中,你应该传递 const& bar 然后复制。在 C++0x 中,您应该传递 bar 然后进行移动(前提是 bar 具有移动构造函数)。

      #include <utility>
      
      struct foo
      {
          bar _b;
      
          foo(bar b) : _b(std::move(b)) {}
      };
      

      如果使用左值参数构造 foo,将调用复制构造函数来创建一个副本 b,并将该副本移动到 _b。如果你用右值参数构造foo,会调用bar的move构造函数移入b,然后又会移入_b

      【讨论】:

      • 我正在考虑这个答案,因为它是最面向未来的,它表明这是 C++98/03 中的一个缺陷,将由 C++0x 解决。但是,请参阅 Alexey Malistov 的回答(以及以下 cmets),了解您可能还希望按值传递 C++98/03 的情况。
      【解决方案4】:

      在风格上,我会说通过引用传递是更好的方式。

      如果性能真的很重要,那就不要猜测了。测量一下。

      【讨论】:

      • +1 用于推荐衡量性能而不是猜测。
      【解决方案5】:

      我还没有检查标准的内容,但通过经验尝试,我可以告诉你,无论构造函数是通过值还是通过 const 引用接受参数,GCC 都不会优化副本。

      但是,如果构造函数采用 const 引用,当您从现有 bar 对象创建 foo 时,它会设法避免不必要的复制。

      总结一下:

      Bar b = makeBar();         // No copy, because of RVO
      FooByValue f1 = makeBar(); // Copy constructor of Bar called once
      FooByRef f2 = makeBar();   // Copy constructor of Bar called once
      FooByValue f3(b);          // Copy constructor of Bar called twice
      FooByRef f4(b);            // Copy constructor of Bar called only once
      

      并不是说我是编译器专家,但我想它通常不能将返回值 RVO 到任意位置(例如 foo 对象的成员字段)是有道理的。相反,它要求目标位于堆栈顶部。

      【讨论】:

        【解决方案6】:

        我假设bar 具有bar(const bar &amp;b) 形式的普通复制构造函数

        在此处获取 const 引用。然后bar 的构造函数将获取该引用,并执行复制。总份数:1。

        如果您关闭引用,编译器将复制 b,将副本传递给 foo 的构造函数,然后将其传递给 bar 并复制它。

        【讨论】:

          【解决方案7】:

          老实说,对于这种情况,最好的选择是让你的构造函数inline,前提是bar 的构造函数不会抛出异常。然后通过引用传递。

          此外,正如您所怀疑的,您的文章不适用于此处。

          【讨论】:

            【解决方案8】:

            在你的班级中持有一个指向 bar 的 shared_pointer 并照此传递。这样你就永远不会调用复制构造函数:)。

            【讨论】:

            • 重量很轻...如果我听说过,担心这将是过早优化的情况。我只是想提供一种不同的方法:)
            猜你喜欢
            • 2012-03-19
            • 1970-01-01
            • 2018-01-18
            • 2012-03-21
            • 2011-05-18
            • 1970-01-01
            • 1970-01-01
            • 2015-03-28
            • 2018-09-22
            相关资源
            最近更新 更多