【问题标题】:is there any specific case where pass-by-value is preferred over pass-by-const-reference in C++?在 C++ 中,是否有任何特定情况下按值传递优于按常量引用传递?
【发布时间】:2010-10-28 06:43:52
【问题描述】:

我读到它们在概念上是平等的。在实践中,是否有任何场合

foo(T t) 

优于

foo(const T& t)

?为什么?


感谢到目前为止的回答,请注意我不是在问 by-ref 和 by-val 之间的区别。

其实我对 by-const-refby-val 之间的区别很感兴趣。

我曾经认为 by-const-ref 可以在调用案例中替换按值,因为即使 Herb Sutter 和 Bjarne 也表示它们在概念上是相等的,并且“by ref”(是 const)意味着更快。直到最近,我在某处读到 by-val 在某些情况下可能会得到更好的优化。

那么何时以及如何?

【问题讨论】:

标签: c++


【解决方案1】:

内置类型和小对象(如 STL 迭代器)通常应按值传递。

这部分是为了增加编译器的优化机会。编译器很难知道引用参数是别名另一个参数还是全局参数 - 它可能必须通过函数多次从内存中重新读取对象的状态,以确保值没有改变。

这就是 C99 的 restrict 关键字的原因(同样的问题,但有指针​​)。

【讨论】:

  • 是的,指针(或就此而言的引用)可能需要比 POD 更多的内存,例如 unsigned char 或 short。在某些环境中这是一个重要的考虑因素(考虑嵌入式),通过另外将变量标记为“const”,您正在向编译器传达您不需要在函数中修改它,以便编译器知道您不需要'不打算在外部或内部修改变量。
  • 在大多数环境中,小于机器寄存器大小的类型(例如 x86 上的 char 和 short)被填充到该大小(例如 32 位)。我想可能存在高度专业化的嵌入式系统,但事实并非如此,但在大多数机器上,使用 char 而不是 char* 不会节省内存。
  • @Jame:谢谢,您能否详细说明在您所说的情况下可以进行哪些优化?我怀疑是这样,但自己无法弄清楚。
【解决方案2】:

如果你想在你的方法体中本地修改t(不影响原来的)(比如在计算过程中),第一种方法会优先。

【讨论】:

  • 但是为什么,因为它们在概念上是相同的?
  • const 说明符将阻止修改,从而使编译器清楚您不打算改变该变量。
【解决方案3】:

如果传递的对象是智能指针(即它自己进行引用计数),那么按值传递可能更合理。

我意识到这是对您问题的一种侧面回答 - 由智能指针包装的对象在按值传递时不会被复制,因此在这种情况下它更类似于通过引用传递。不过,在这种情况下,您不需要“通过引用”语义。

到目前为止,我的推理路线存在问题 - 通过按值传递,您将失去论点的“常量性”。也许您毕竟应该只使用“按引用”语义......

【讨论】:

  • 智能指针在复制时通常需要锁定 - 智能指针上的值语义是一场噩梦,
  • +1 @xtofl 复制智能指针可能会导致巨大的性能损失。像任何其他昂贵的复制类型一样通过 const 引用传递它们。
【解决方案4】:

不要忘记在某些情况下存在差异 - 当您处理具有奇怪复制/赋值语义的对象时。

auto_ptr<> 是一个典型的例子——通过价值传递而不考虑后果,你最终可能会一团糟。

【讨论】:

    【解决方案5】:

    另外,当 T 是简单类型(int、bool 等)时,通常使用 foo(T t)。

    【讨论】:

      【解决方案6】:

      另一个没有提到的情况是对象的大量使用。假设您传递了一个具有 5 个整数的结构作为成员。如果您要在函数中大量访问所有 5 个,那么在某个时候取消引用成本超过了复制成本。但是,您必须运行探查器才能知道是什么时候。

      不过,我应该指出,像 STL 容器这样在堆上分配内存的东西,如果可以避免的话,几乎不应该按值传递,因为与堆栈分配相比,堆分配非常慢。

      【讨论】:

      • 两种情况下的取消引用成本相同。如果您经常使用结构成员,则结构指针几乎肯定会存储在寄存器中,因此将通过 8(%edx) 之类的东西访问其成员,而对于局部变量x86。
      【解决方案7】:

      在这种情况下,您没有其他选择,只能按值传递参数。当然 boost 确实可以处理这个问题。但是如果没有 boost,我们必须传递一个 Value 来按值运行。

      class Test
      {
      public:
          Test()
          {
              std::set<std::string> values;
              values.insert("A");
              values.insert("V");
              values.insert("C");
      
              std::for_each(values.begin(), values.end(), 
                      bind1st(mem_fun(&Test::process), this));
          }
      
      private:
          void process( std::string value )
          {
              std::cout << "process " << value << std::endl;
          }
      };
      

      【讨论】:

        【解决方案8】:

        const 引用传递和值传递在概念上相同的原因是两者都不能修改原始内容。

        通常,我非常喜欢按值传递,因为它创建的代码避免了多个线程共享对公共数据的访问时发生的许多复杂性。

        也就是说,它确实可能会使您的代码变慢。我过去的看法一直是更喜欢按值传递,除非我知道他们这样做是(或将是)性能问题。我可能需要稍微修改一下,以包含通过 const 引用作为更好的选择。

        【讨论】:

          【解决方案9】:

          如果函数最直接的实现涉及在本地修改参数值,那么通过值而不是通过 const 引用传递它是有意义的

          例如strcpy的一行版本:

          char *strcpy(char *dest, const char *src)
          {
             while (*dest++ = *src++);
          
             return s1;
          }
          

          如果您将指针作为 const 引用,您需要将它们复制到程序主体中的临时对象,而不是让参数传递机制为您完成。

          【讨论】:

            【解决方案10】:

            两个非常具体的案例:

            当您编写具有强异常保证的赋值运算符时,您可以像这样编写它们

            X& X::operator=(const X& orig)
            {
                X tmp(orig);
                swap(this, tmp);
                return *this;
            }
            

            或者您可以认识到,发生的第一件事是您制作了一个副本,然后将其作为通话的一部分完成

            X& X::operator=(X tmp)
            {
                swap(this, tmp);
                return *this;
            }
            

            如果您有一个具有所有权语义的智能指针,例如auto_ptr,并且您想将所有权转移给被调用的函数,您应该按值传递它。当然,现在有 8 个人会很快指出您可能不想使用 auto_ptr,他们可能是对的,但有时您不会做出这样的选择。

            虽然一点也不具体,但我经常最终会按值传递较小的对象,因为这样可以节省我在堆上的分配。不仅实际分配和最终解除分配需要时间,而且通过指针引用所有内容也无助于改善数据局部性。换句话说,它可能会对您的表现产生影响。收支平衡点的确切位置取决于您的应用程序,但我个人会毫不犹豫地传递一个指针大小较大的对象。

            【讨论】:

              【解决方案11】:

              它们在概念上根本不相等......

              前者在函数内部创建对象的副本。这意味着可以安全地在函数中修改该值。这也意味着发生了对象的完整副本,如果对象很大,这可能是一个问题。

              后者为对象创建一个别名,并声明它不能在对象内修改。不会发生复制,但每次访问函数内的对象都需要取消引用。编译器会为我们处理这个问题,但知道这一点仍然很重要。

              如果您有一个通常在寄存器中传递的类型,则差异变得非常重要。例如,整数、浮点数,甚至某些平台上的 4 浮点向量。性能问题表明您希望对象尽可能长时间地保留在寄存器中,而不会将自身写回内存,而按值传递则更有可能。

              所以对于基本类型(char、short、int、long、float、double),您应该始终更喜欢按值传递除非您特别需要使用引用来存储值以供之后使用函数退出。对于完整对象,一般更喜欢通过 const 引用传递。

              【讨论】:

                【解决方案12】:
                1. 如前所述,如果您希望在函数中获得对象的副本,则首选按值传递。
                2. 如果复制 T 比创建/复制引用便宜,我通常使用按值传递,例如T=char,T=短。此处的好处可能取决于平台,并且您可能仍希望 const 在适用的情况下帮助优化器。

                【讨论】:

                  【解决方案13】:

                  某些例程需要副本,因此不应通过引用传递。例如,国际象棋程序的移动生成器可能需要当前位置的副本来处理(递归),而不是实际修改位置的原始实例。

                  【讨论】:

                  • 我还是更喜欢通过 const ref 传递。被调用的代码可能具有不需要副本的代码路径。它们可以对 const ref 进行操作并返回,而无需复制的开销。然后函数中的其他代码路径可以在他们发现需要时创建一个副本。
                  【解决方案14】:

                  this answer 中回答。这是该答案的SymLink。请在那里投票,如果有的话:)

                  【讨论】:

                    【解决方案15】:

                    Boost.CallTraits 是一种鲜为人知但有用的工具,用于执行参数和结果传递,它应该是相关类型可用的最有效方法。

                    【讨论】:

                      猜你喜欢
                      • 2022-01-21
                      • 2014-04-08
                      • 1970-01-01
                      • 2023-03-02
                      • 2010-09-05
                      相关资源
                      最近更新 更多