【问题标题】:Returning a const reference to an object instead of a copy返回对对象的 const 引用而不是副本
【发布时间】:2010-09-13 04:18:56
【问题描述】:

在重构一些代码时,我遇到了一些返回 std::string 的 getter 方法。例如这样的事情:

class foo
{
private:
    std::string name_;
public:
    std::string name()
    {
        return name_;
    }
};

当然,getter 会更好地返回const std::string&?当前方法正在返回一个效率不高的副本。返回一个 const 引用会导致任何问题吗?

【问题讨论】:

  • 谁说没有效率。在 std::string 上做了很多工作来使这个操作高效。传递引用和传递字符串是有区别的,但实际区别通常是微不足道的。
  • @Loki:没有人说它会高效,事实上最新版本的 C++ 说它可能会高效。使该代码合理高效的唯一优化是引用计数的 std::string。但是,许多 STL 实现不进行引用计数,因为在现代多核 CPU 上,管理引用计数比您想象的要慢得多。所以是的,返回副本要慢得多。多年来,GCC 和微软的 STL 都没有对 std::string 进行引用计数。
  • @seanmiddleditch:在你假设之前总是测量(这就是我所说的)。编译器还可以应用许多其他优化:内联和复制省略是两个可以将成本降低到零的。
  • @Loki:是的,这是个好建议;总是配置文件。同样好的建议是了解“假设”和“事实”之间的区别。我向您保证,Rob 的代码在任何符合 C++ 编译器的实现中都不会导致零开销。该语言要求从 foo::name() 返回时必须至少调用一次复制构造函数,即使使用内联(对何时调用构造函数没有影响)、复制省略(不适用于此代码)和 RVO(可能会减少额外的复制构造函数调用,但不会全部消除)。
  • @Loki:我 确实 尝试在 GCC 4.6 和 VC 10 上测量它,都打开了最大优化,并得到相同的结果:复制构造函数是调用。即使您考虑 as-if 规则,这也是“明显”的结果,因为 std::string 的复制构造函数实际上有副作用:它要么创建一个全新的字符数组和字符串的副本,要么它操纵引用计数值(取决于实现)。请注意,在引用计数的 impl 上进行足够简单的微基准测试可能不会在这里给您“真实”的结果,这是我第一次尝试在 GCC 上遇到的。

标签: c++ constants


【解决方案1】:

我会将其更改为返回 const std::string&。如果您不更改所有调用代码,调用者可能会复制结果,但不会引入任何问题。

如果您有多个线程调用 name(),则会出现一个潜在的问题。如果您返回一个引用,但随后更改了基础值,那么调用者的值将会更改。但是现有的代码无论如何看起来都不是线程安全的。

看看 Dima 对一个相关的潜在但不太可能的问题的回答。

【讨论】:

    【解决方案2】:

    取决于你需要做什么。也许您希望所有调用者在不更改类的情况下更改返回值。如果您返回不会飞的 const 引用。

    当然,下一个参数是调用者可以制作自己的副本。但是,如果您知道如何使用该函数并且知道无论如何都会发生这种情况,那么这样做可能会在后面的代码中为您节省一步。

    【讨论】:

    • 我认为这不是问题。调用者只需执行“std::string s = aFoo.name()”,s 将是一个可变副本。
    【解决方案3】:

    可以想象,如果调用者真的想要一个副本,您可能会破坏某些东西,因为他们即将更改原件并想要保留它的副本。然而,实际上它更有可能只返回一个 const 引用。

    最简单的做法是尝试它,然后测试它是否仍然有效,前提是您有某种可以运行的测试。如果没有,我会先专注于编写测试,然后再继续重构。

    【讨论】:

      【解决方案4】:

      这可能导致问题的唯一方法是调用者存储引用,而不是复制字符串,并在对象被销毁后尝试使用它。像这样:

      foo *pFoo = new foo;
      const std::string &myName = pFoo->getName();
      delete pFoo;
      cout << myName;  // error! dangling reference
      

      但是,由于您现有的函数返回一个副本,那么您将 不要破坏任何现有的代码。

      编辑: 现代 C++(即 C++11 及更高版本)支持 Return Value Optimization,因此不再反对按值返回内容。仍然应该注意按值返回非常大的对象,但在大多数情况下应该没问题。

      【讨论】:

      • 不会破坏现有代码,但不会获得任何性能优势。如果问题像您的示例中一样容易发现,那很好,但通常会更棘手。例如,如果您有 vector 和 push_back,导致调整大小等。为时过早优化等等等等。
      • @Dima:很可能大多数编译器都会在这种情况下执行命名值返回值优化。
      • @Richard:是的,我最近读到过。但这仅涵盖当您立即将返回值分配给某物时的情况,对吗?如果将返回值传递给函数怎么办?临时工的建设还会优化吗?
      • 那么最后的判决是什么?更好地传递参考还是只传递副本?让我们假设编译器是愚蠢的,而程序员实际上必须像我们变得懒惰的前一天那样具体和深思熟虑:)
      • @Dale,我建议返回一个 const 引用。
      【解决方案5】:

      如果您更改为 const 引用,该函数的典型用法不会中断的可能性非常好。

      如果调用该函数的所有代码都在您的控制之下,只需进行更改,看看编译器是否会报错。

      【讨论】:

        【解决方案6】:

        我通常返回 const& 除非我不能。 QBziZ 举例说明了这种情况。当然,QBziZ 还声称 std::string 具有写时复制语义,这在今天很少是真的,因为 COW 在多线程环境中涉及大量开销。通过返回 const & 你让调用者有责任用他们末尾的字符串做正确的事情。但是由于您正在处理已经在使用的代码,因此您可能不应该更改它,除非分析表明该字符串的复制会导致大量性能问题。然后,如果您决定更改它,您将需要彻底测试以确保您没有破坏任何东西。希望与您合作的其他开发人员不要像 Dima 的回答那样做粗略的事情。

        【讨论】:

          【解决方案7】:

          这有关系吗?只要您使用现代优化编译器,按值返回的函数就不会涉及副本,除非它们在语义上需要这样做。

          请参阅the C++ lite FAQ

          【讨论】:

          • 按值返回优化是否如此复杂以至于它会使用指向原始成员值 i.o. 的指针/引用。的副本?我认为情况并非如此。 Rbv 优化跳过了返回值的几个步骤,但它仍然会调用 cstring 的复制 ctor,尽管是就地版本。
          • 如果函数返回一个类成员变量的副本,如本例所示,那么它确实会返回一个副本。原始成员不能在进程中被销毁(不像一个按值返回并创建一个临时返回的函数)
          【解决方案8】:

          const 引用返回的一个问题是,如果用户编写如下代码:

          const std::string & str = myObject.getSomeString() ;
          

          使用 std::string 返回时,临时对象将保持活动状态并附加到 str,直到 str 超出范围。

          但是const std::string &amp; 会发生什么?我的猜测是我们会有一个对象的 const 引用,当它的父对象释放它时,它可能会死掉:

          MyObject * myObject = new MyObject("My String") ;
          const std::string & str = myObject->getSomeString() ;
          delete myObject ;
          // Use str... which references a destroyed object.
          

          所以我更喜欢 const 引用返回(因为无论如何,我只是更愿意发送引用而不是希望编译器优化额外的临时),只要遵守以下合同:“如果你想要它超越我的对象的存在,他们在我的对象被破坏之前复制它”

          【讨论】:

            【解决方案9】:

            std::string 的一些实现使用写时复制语义共享内存,因此按值返回几乎与按引用返回一样有效你没有担心生命周期问题(运行时为您完成)。

            如果您担心性能,那么对其进行基准测试

            你知道他们对做出假设的看法......

            【讨论】:

            • 相反,您在修改不相关的字符串时必须担心线程安全:)
            • 嘿。运行时给出了什么,多线程带走了。 :)
            • 出于好奇——g++ 是否使用写时复制语义实现 std::string?
            • @user60628 COW 已经失宠(十年前)见this article
            • C++11 及更高版本实际上不允许使用 COW。
            【解决方案10】:

            好的,那么返回副本和返回引用之间的区别是:

            • 性能:返回参考可能更快,也可能不会更快;这取决于您的编译器实现如何实现std::string(正如其他人指出的那样)。但即使你返回引用,函数调用后的赋值通常也会包含一个副本,如std::string name = obj.name();

            • 安全:返回引用可能会也可能不会导致问题(悬空引用)。如果您的函数的用户不知道他们在做什么,将引用存储为引用并在提供对象超出范围后使用它,那么就会出现问题。

            如果您想要 快速且安全,请使用 boost::shared_ptr。您的对象可以在内部将字符串存储为shared_ptr 并返回shared_ptr。这样,就不会复制对象,而且它始终是安全的(除非您的用户使用 get() 提取原始指针并在您的对象超出范围后对其进行处理)。

            【讨论】:

              【解决方案11】:

              实际上,另一个特别是通过引用返回字符串not的问题是std::string 提供了通过@ 指向内部const char* 的指针访问987654321@ 方法。这让我头疼了好几个小时。例如,假设我想从 foo 中获取名称,并将其传递给 JNI 以用于构造 jstring 以稍后传递到 Java,并且 name() 返回一个副本而不是引用。我可能会这样写:

              foo myFoo = getFoo(); // Get the foo from somewhere.
              const char* fooCName = foo.name().c_str(); // Woops!  foo.name() creates a temporary that's destructed as soon as this line executes!
              jniEnv->NewStringUTF(fooCName);  // No good, fooCName was released when the temporary was deleted.
              

              如果你的调用者要做这种事情,最好使用某种类型的智能指针,或者一个常量引用,或者至少在你的 foo.name( ) 方法。我提到 JNI 是因为以前的 Java 编码人员可能特别容易受到这种看似无害的方法链的攻击。

              【讨论】:

              • 该死的,好收获。虽然我认为 C++11 尊重了临时代码的生命周期,使这段代码对任何兼容的编译器都有效。
              • @MarkMcKenna 很高兴知道。我做了一些搜索,这个答案似乎表明您仍然需要小心一点,即使使用 C++ 11:stackoverflow.com/a/2507225/13140
              【解决方案12】:

              返回对成员的引用会公开类的实现。 这可能会阻止更改课程。如果需要优化,可能对私有或受保护的方法有用。 What should a C++ getter return

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-06-29
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多