227

在 C++ 中通过值传递还是通过常量引用传递更好?

我想知道哪个是更好的做法。我意识到通过常量引用传递应该在程序中提供更好的性能,因为您没有制作变量的副本。

4

11 回答 11

216

通常建议最佳实践1所有类型使用 pass by const ref ,除了内置类型(, ,等),迭代器和函数对象charintdouble(lambdas,派生自的类std::*_function)。

在移动语义存在之前尤其如此。原因很简单:如果您按值传递,则必须制作对象的副本,并且除了非常小的对象外,这总是比传递引用更昂贵。

使用 C++11,我们获得了移动语义。简而言之,移动语义允许在某些情况下,一个对象可以“按值”传递而不复制它。特别是,当您传递的对象是rvalue时,就是这种情况。

就其本身而言,移动对象仍然至少与通过引用传递一样昂贵。然而,在许多情况下,一个函数无论如何都会在内部复制一个对象——即它将获得参数的所有权2

在这些情况下,我们有以下(简化的)权衡:

  1. 我们可以通过引用传递对象,然后在内部复制。
  2. 我们可以按值传递对象。

“按值传递”仍然会导致对象被复制,除非对象是右值。在右值的情况下,可以移动对象,因此第二种情况突然不再是“复制,然后移动”,而是“移动,然后(可能)再次移动”。

对于实现正确移动构造函数的大型对象(例如向量、字符串……),第二种情况比第一种更有效因此,如果函数拥有参数的所有权,并且对象类型支持高效移动,则建议使用按值传递。


历史注释:

事实上,任何现代编译器都应该能够找出何时按值传递代价高昂,并尽可能隐式地将调用转换为使用 const ref。

理论上。在实践中,编译器不能总是在不破坏函数的二进制接口的情况下改变它。在某些特殊情况下(当函数被内联时),如果编译器能够确定原始对象不会通过函数中的操作进行更改,则实际上会省略副本。

但总的来说,编译器无法确定这一点,而 C++ 中移动语义的出现使这种优化的相关性大大降低。


1例如,Scott Meyers,Effective C++

2对于对象构造函数来说尤其如此,它可以接受参数并将它们存储在内部,作为构造对象状态的一部分。

于 2008-11-06T21:49:23.980 回答
99

编辑: Dave Abrahams 在 cpp-next 上的新文章:

想要速度?按值传递。


对于复制成本较低的结构,按值传递具有额外的优势,即编译器可能会假设对象没有别名(不是相同的对象)。使用传递引用,编译器不能总是假设。简单的例子:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

编译器可以将其优化为

g.i = 15;
f->i = 2;

因为它知道 f 和 g 不共享相同的位置。如果 g 是一个引用 (foo &),编译器就无法假设。因为 gi 可以被 f->i 别名化,并且必须具有 7 的值。所以编译器必须从内存中重新获取 gi 的新值。

有关更实用的规则,请参阅Move Constructors文章中的一组很好的规则(强烈推荐阅读)。

  • 如果函数打算将参数更改为副作用,请通过非常量引用获取它。
  • 如果函数不修改其参数并且参数是原始类型,则按值获取。
  • 否则通过 const 引用获取,但以下情况除外
    • 如果该函数无论如何都需要复制 const 引用,请按值获取。

上面的“原始”基本上是指几个字节长并且不是多态(迭代器、函数对象等)或复制成本高的小数据类型。在那篇论文中,还有另一条规则。这个想法是,有时人们想要复制一份(以防参数无法修改),有时则不想(以防万一,如果参数是临时的,则想在函数中使用参数本身, 例如)。该论文详细解释了如何做到这一点。在 C++1x 中,该技术可以在语言支持下本地使用。在那之前,我会遵守上述规则。

示例:要使字符串大写并返回大写版本,应始终按值传递:无论如何都必须获取它的副本(不能直接更改 const 引用)-因此最好使其尽可能透明调用者并尽早制作该副本,以便调用者可以尽可能地优化 - 如该论文中所述:

my::string uppercase(my::string s) { /* change s and return it */ }

但是,如果您无论如何都不需要更改参数,请通过引用 const 来获取它:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

但是,如果您参数的目的是在参数中写入一些东西,那么通过非常量引用传递它

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}
于 2008-11-07T05:52:25.283 回答
12

取决于类型。您正在增加必须进行引用和取消引用的小开销。对于大小等于或小于使用默认复制 ctor 的指针的类型,按值传递可能会更快。

于 2008-11-06T21:47:18.413 回答
9

正如已经指出的那样,这取决于类型。对于内置数据类型,最好按值传递。即使是一些非常小的结构,例如一对整数,也可以通过按值传递来更好地执行。

这是一个示例,假设您有一个整数值并且您想将它传递给另一个例程。如果该值已被优化为存储在寄存器中,那么如果您想将其作为引用传递,则必须首先将其存储在内存中,然后将指向该内存的指针放在堆栈上以执行调用。如果它是按值传递的,那么只需要将寄存器压入堆栈即可。(细节比给定不同的调用系统和CPU要复杂一些)。

如果你在做模板编程,你通常会被迫总是通过 const ref 传递,因为你不知道传入的类型。传递错误值的惩罚比传递内置类型的惩罚要糟糕得多通过常量参考。

于 2008-11-06T21:56:34.573 回答
7

这是我在设计非模板函数的接口时通常的工作方式:

  1. 如果函数不想修改参数并且值复制起来很便宜(int、double、float、char、bool 等...请注意 std::string、std::vector 和其余部分,则按值传递标准库中的容器不是)

  2. 如果值复制成本很高并且函数不想修改指向的值并且 NULL 是函数处理的值,则传递 const 指针。

  3. 如果值复制成本很高并且函数想要修改指向的值并且 NULL 是函数处理的值,则通过非常量指针传递。

  4. 当值复制成本高且函数不想修改引用的值时,通过 const 引用传递,如果使用指针代替,NULL 将不是有效值。

  5. 当值复制成本很高并且函数想要修改所引用的值并且如果使用指针代替,NULL 将不是有效值时,通过非常量引用传递。

于 2015-06-11T07:02:41.710 回答
6

听起来你得到了答案。按值传递是昂贵的,但如果您需要,它会给您一个副本以供使用。

于 2008-11-06T21:44:24.863 回答
4

通常,通过 const 引用传递更好。但是,如果您需要在本地修改函数参数,则最好使用按值传递。对于某些基本类型,按值传递和按引用传递的性能通常相同。实际上引用内部由指针表示,这就是为什么您可以期望例如指针的两个传递在性能方面是相同的,或者由于不必要的取消引用,甚至按值传递可以更快。

于 2008-11-06T21:50:21.077 回答
2

通过值传递小类型。

传递大类型的 const 引用(大的定义可能因机器而异)但是,在 C++11 中,如果您要使用数据,则按值传递,因为您可以利用移动语义。例如:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

现在调用代码会做:

Person p(std::string("Albert"));

并且只会创建一个对象并将其直接移动到name_class 中的 member 中Person。如果您通过 const 引用传递,则必须制作一个副本以将其放入name_.

于 2013-10-29T13:48:09.627 回答
1

根据经验,非类类型的值和类的 const 引用。如果一个类真的很小,那么按值传递可能会更好,但差异很小。您真正要避免的是按值传递一些巨大的类并将其全部复制 - 如果您传递一个包含相当多元素的 std::vector ,这将产生巨大的影响。

于 2008-11-06T22:04:00.300 回答
-4

通过引用传递比通过值传递更好。我正在解决 Leetcode 上最长的公共子序列问题。它显示 TLE 以通过值传递,但接受了代码以通过引用传递。我花了 30 分钟才弄清楚这一点。

于 2021-04-24T08:39:21.710 回答
-6

简单的区别:-在函数中我们有输入和输出参数,所以如果您传递的输入和输出参数相同,则使用引用调用,否则如果输入和输出参数不同,则最好使用值调用。

例子 void amount(int account , int deposit , int total )

输入参数:account,deposit输出参数:total

输入和输出是不同的使用 vaule 调用

  1. void amount(int total , int deposit )

输入总额 存款 输出总额

于 2015-01-05T04:06:52.197 回答