【问题标题】:What reasons are there for a returning by value in C++在 C++ 中按值返回的原因是什么
【发布时间】:2013-08-25 16:07:15
【问题描述】:

由于在大多数情况下按值返回比返回引用效率低,有哪些按值返回有意义或者是唯一可能的方法的例子?

编辑:我的问题措辞不正确。我知道对于内置类型,按值返回通常更有效。我主要指的是返回大于指针的用户定义类型的情况。

【问题讨论】:

  • 是什么让您认为它的效率较低?你看过编译器生成什么吗?
  • 按值返回在大多数情况下比返回引用效率低不是真的,总是有返回值优化。
  • 如果函数创建了一个新的(堆栈分配的)该类型的值,那么你不能通过引用返回,因为这个引用是无效的。例如,查看 string::substr ,它按值返回一个新的字符串对象。
  • 祝你好运,通过引用返回函数本地的变量...
  • 由于在大多数情况下按引用返回的正确性不如按值返回,有哪些例子可以说明按引用返回是有意义的或者是唯一可能的方法?

标签: c++


【解决方案1】:

您在这里遗漏了重点——通过引用返回并不总是有意义的,也不总是合法的。通过引用返回本地会导致未定义的行为。您可以通过引用返回一个超出函数范围的变量,可以是:

  • 动态分配 - 坏主意,您必须自己管理生命周期
  • 类的成员 - 这通常与具有 2 个版本的 getter 一起使用:一个返回引用并且您可以修改成员或返回 const 引用的 const getter
  • static 局部变量(或任何具有静态存储的变量) - 通常情况下,仅用于通过引用返回的函数中不需要此变量。

NRVO 使按价值回报在实践中可行,因此除非您有明确的测量结果表明按价值回报是一个瓶颈,否则不要担心。而是担心两者所暗示的语义。

【讨论】:

    【解决方案2】:

    按值返回效率不低在两种情况下:1如果您正在移动一个对象 (C++11) 和 2 > 如果编译器可以省略复制或优化返回值(对于内置类型总是可能的,另请参阅 JaredPar 的答案)。

    几个优点1不需要在函数调用之前声明变量(这可能并不总是可能的),并且可以通过以下方式检测返回的变量类型auto2设置返回变量的意图很明显。

    void foo(some_type &obj_ref);
    some_type bar();
    
    // using reference
    some_type A;          // requires default constructor for A
    foo(A);               // modification of A is not obvious
    
    // returning a value
    auto B = bar();       // move or copy construction of B
    

    请注意,最后一条语句可以使用返回值优化或移动语义 (C++11)

    【讨论】:

    • 第一段中的案例 #1 过于笼统(因此不正确)。
    • 忽略 C++ 移动语义,指望 RVO 是一种好的做法吗?这不是远离标准并依赖于编译器特定的行为吗?您如何提前知道编译器会为您优化某些东西,从表面上看,可以通过更改设计并返回参考来改进?
    • 对于一些“改进”的价值。与编译器的斗争很少有好的结果。
    • 你应该意识到你也依赖于编译器特定的行为:你假设一个编译器如果你通过引用返回会产生更好的代码(大多数时候这是错误的,不是快或慢,但错了,虽然我假装这不仅仅是为了让你沉迷于你的讨论)。事实是,如果你关心性能,但不考虑编译器,那你就错了。
    【解决方案3】:

    这是 g++ 在将 -O3 标志用于通过引用返回整数的方法时默认生成的:

    void Foo(int& rc) {
        rc = 42;
    }
    

    变成:

    __Z3FooRi:
    Leh_func_begin1:
        pushq   %rbp
    Ltmp0:   
        movq    %rsp, %rbp
    Ltmp1:
        movl    $42, (%rdi)
        popq    %rbp
        ret
    Leh_func_end1:
    

    这是按值返回的相同方法:

    int Foo() {
      return 42;
    }
    

    变成:

    __Z3Foov:
    Leh_func_begin1:
        pushq   %rbp
    Ltmp0:
        movq    %rsp, %rbp
    Ltmp1:
        movl    $42, %eax
        popq    %rbp
        ret
    Leh_func_end1:
    

    如您所见,您的前提是有缺陷的。两个版本产生几乎相同的机器代码。

    三十年来,人们一直在为 C/C++ 开发优化编译器。您可以假设他们已经想到了任何常见的用例,并尽其所能确保任何常见的代码模式都能生成最佳代码。永远不要改变你的编码风格来提高性能,除非你掌握了能够明确证明它存在差异的分析结果。太多此类“优化”不仅使代码更难阅读,而且实际上使性能更差。

    编辑

    这是与更大类型的类似比较。在这里,两种方法都返回以下struct

    struct Bar
    {
        unsigned int a;
        unsigned int b;
        unsigned int c;
        unsigned int d;
    };
    

    这段代码:

    void Foo(Bar& rc) {
        rc.a = 1;
        rc.b = 2;
        rc.c = 3;
        rc.d = 4;
    }
    

    产生这个输出:

    __Z3FooR3Bar:
    Leh_func_begin1:
        pushq   %rbp
    Ltmp0:
        movq    %rsp, %rbp
    Ltmp1:
        movl    $1, (%rdi)
        movl    $2, 4(%rdi)
        movl    $3, 8(%rdi)
        movl    $4, 12(%rdi)
        popq    %rbp
        ret
    

    Leh_func_end1:

    另一方面,这个按值返回的版本:

    Bar Foo() {
        Bar rc;
        rc.a = 1;
        rc.a = 2;
        rc.a = 3;
        rc.a = 4;
    
        return rc;
    }
    

    最终产生更少机器代码指令:

    __Z3Foov:
    Leh_func_begin1:
        pushq   %rbp
    Ltmp0:
        movq    %rsp, %rbp
    Ltmp1:
        movl    $4, %eax
        xorl    %edx, %edx
        popq    %rbp
        ret
    Leh_func_end1:
    

    如果您想自己在 g++ 中进行这些比较,请使用 g++ -S -O3。其他编译器将具有类似的生成程序集的方法。

    【讨论】:

    • 请查看我对沃尔特回答的评论,以了解我来自哪里。决定一个函数是按值返回还是按引用返回的一个好的经验法则是什么?
    • 第一条经验法则:永远不要在不查看分析器的时序或检查实际编译器输出的情况下根据性能做出编码决定。
    【解决方案4】:

    我认为您从错误的假设开始这个问题,即按价值返回的效率低于其他形式的返回。出于几个原因,情况并非如此

    • intchar 等简单的内置类型可以通过寄存器返回
    • 由于命名返回值优化 (NRVO),即使是大值也可以按值高效返回

    在某些情况下,按值返回是低效的。然而,这并不意味着所有情况都是如此。

    【讨论】:

      【解决方案5】:

      初学者的答案:

      如果我不需要更改我传递的变量但只希望函数的解决方案的值用于打印值之类的目的并且我不需要将答案存储在任何地方,则按值返回是有意义的!

      只是想帮忙!

      【讨论】:

        【解决方案6】:

        你的假设是有争议的(已经给出了 cmets),但是对于这个问题:我建议阅读

        Scott Meyers 的 Effective C++(第三版)
        第 21 条:当必须返回对象时,不要试图返回引用。

        示例为(稍作修改):

        class Rational {
        public:
            Rational(int numerator = 0, int denominator = 1)
                : n(numerator), d(denominator) {}
        
            // ...
        
        private:
            int n, d;
        
            friend Rational operator*(const Rational& lhs, const Rational& rhs);
        };
        

        用法是这样的:

            Rational a(1, 2);    // a = 1/2
            Rational b(3, 5);    // b = 3/5
        
            Rational c = a * b;  // c should be 3/10
        

        以及实现:

        Rational operator*(const Rational& lhs, const Rational& rhs)
        {
            Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
            // Would you return that local variable by reference?
            //  You don't want a dangling reference...
            //  Returning by value is the correct thing to do.
            return result;
        }
        

        或更短:

        Rational operator*(const Rational& lhs, const Rational& rhs)
        {
            return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
        }
        

        (您可以将其设为inline 并将其放在标题中)。

        阅读完整的条目以了解“替代方案”(动态分配、静态变量...)以及为什么它们都不好(这就是重点:它们都不好:有点危险,甚至完全不正确)。

        最后,从那里引用:

        编写必须返回新对象的函数的正确方法是 让该函数返回一个新对象。

        所以:当它是自然语义时,只需按值返回。

        【讨论】:

          【解决方案7】:

          例如,当您想要修改返回的对象而不影响调用成员函数的对象时,您可以按值返回。在以下示例中,您可以修改b(由值返回)而不更改对象a(以及其中的值_val):

          #include<iostream>
          using namespace std;
          
          class A{
          public:
              A(int val) : _val(val){}
              int val(){return _val;}
          private:
              int _val;
          };
          
          int main()
          {
                  A a(3);
                  int b = a.val();
                  ++b;
                  cout << "a.val() = " << a.val() << endl;
                  cout << "b = " << b << endl;
          }
          

          more reasons

          【讨论】:

          • @milleniumbug,我的意思是你可以修改返回的对象,同时保持容器内的对象不变。谢谢,这之前不清楚
          • 好的,现在答案已经更正,我删除了反对票。
          猜你喜欢
          • 1970-01-01
          • 2016-10-16
          • 1970-01-01
          • 1970-01-01
          • 2011-02-04
          • 1970-01-01
          • 2011-05-15
          • 1970-01-01
          • 2022-12-20
          相关资源
          最近更新 更多