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

238
在C++中,通过传值还是通过引用传递(加上const)更好呢?
我想知道哪种方式更好的实践方法。我认识到通过引用传递(加上const)可以提供更好的程序性能,因为你不需要复制变量。

相关: https://dev59.com/lnI95IYBdhLWcg3w1BdN#2139254 - sbi
11个回答

236

曾经普遍建议最佳实践是对于除了内置类型(charintdouble等)、迭代器和函数对象(lambda、从std::*_function派生的类)之外的所有类型都使用常量引用传递。1

在存在移动语义之前,这尤其正确。原因很简单:如果你通过值传递,那么必须复制该对象,除非是非常小的对象,否则始终比传递引用更昂贵。

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

本质上,移动对象仍然至少与通过引用传递一样昂贵。但是,在许多情况下,函数将在内部复制对象——即它将拥有该参数。2

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

  1. 我们可以通过引用传递对象,然后在内部复制。
  2. 我们可以按值传递对象。
“按值传递”仍会导致对象被复制,除非该对象是右值。对于右值,该对象可以被移动,因此第二种情况不再是“先复制,然后移动”,而是“先移动,然后(可能)再次移动”。
对于实现了适当的移动构造函数的大型对象(例如向量、字符串等),第二种情况比第一种情况要高效得多。因此,建议在函数获取参数所有权并且对象类型支持高效移动时使用按值传递。

历史注释:

事实上,任何现代编译器都应该能够判断传值调用是否昂贵,并在可能的情况下自动将调用转换为使用const引用。

理论上而言。 实际上,编译器不能总是更改此操作而不破坏函数的二进制接口。在某些特殊情况下(当函数被内联时),如果编译器可以确定原始对象不会通过函数中的操作发生更改,则拷贝实际上将被省略。

但是通常情况下,编译器无法确定这一点,C++中移动语义的出现使得这种优化变得不那么相关了。


1 例如,在Scott Meyers的Effective C++中。

2 这在对象构造函数中尤其常见,它们可能会接受参数并将它们存储在内部,以成为构建对象状态的一部分。


4
跟往常一样,Boost 在这里提供了帮助。http://www.boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm 有模板内容可以自动判断一个类型是否是内置类型(在模板中特别有用,因为有时候你不容易知道一个类型是否为内置类型)。 - CesarB
18
这个答案漏掉了一个重要的点。为了避免切片问题,你必须通过引用传递参数(无论是const还是其他方式)。请参考https://dev59.com/YHVC5IYBdhLWcg3wfxQ8。 - ChrisN
8
@Chris:没错,我忽略了多态的整个部分,因为那是完全不同的语义。我相信OP(从语义上)指的是"按值"传递参数。当需要其他语义时,问题甚至不会出现。 - Konrad Rudolph
1
在这段文字的背景下,“take ownership”并不意味着从另一个名称中取走所有权,而仅仅是承担它,即之后拥有该资源。重要的不是(副本)资源是否在其他地方拥有,而仅仅是函数是否也需要所有权。当然,如果参数是临时的,则逻辑上会创建一个副本。但是,如果参数不是临时的,则不会创建实际的副本;相反,该值将被移动。如果该值不是临时的,则无法使用rvalue引用(除非调用者使用std::move)。 - Konrad Rudolph
1
@experimentunit1998X 是的,绝对没错。实际上,在现代C++中,通过非const引用传递应该是非常罕见的:大多数函数不应直接修改它们的参数,而是返回修改后的值作为副本。这通常会导致更安全、更易于维护的代码(当然,出于性能原因也有例外)。 - Konrad Rudolph
显示剩余3条评论

104
编辑: 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&),编译器就不能做出这样的假设。因为g.i可能会被f->i别名化并且必须有一个值为7。所以编译器必须重新从内存中获取g.i的新值。

对于更实际的规则,可以在Move Constructors文章中找到一组很好的规则(强烈推荐阅读)。

  • 如果函数意图通过副作用改变参数,则通过非const引用传递参数。
  • 如果函数不修改其参数并且参数是原始类型,则通过值传递参数。
  • 否则,通过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 */
}

然而,如果参数的目的是将某些内容写入到参数中,则应该通过非const引用进行传递。

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}

我认为你的规则很好,但是我对第一部分不太确定,因为你说不将其作为引用传递会加快速度。是的,没错,但仅仅因为优化而不将某些东西作为引用传递根本没有意义。如果你想要改变传入的堆栈对象,请使用引用传递。如果你不想改变它,请按值传递。如果你不想改变它,就按const-ref传递。通过按值传递获得的优化应该不重要,因为当作为引用传递时,你会获得其他东西。我不明白“想要速度?”这句话的意思,因为如果你要执行这些操作,你肯定会按值传递。 - chikuba
Johannes:我在阅读这篇文章时非常喜欢它,但当我尝试运行代码时却感到失望。这段代码 在 GCC 和 MSVC 上都失败了。我是错过了什么,还是它实际上不起作用? - user541686
我不认为如果你想要复制一个对象,就应该通过值传递它(而不是const引用),然后再移动它。从这个角度来看,什么更有效率,复制和移动(甚至可以有两个复制品,如果你将其向前传递),还是只有一个复制品?是的,有一些特殊情况需要考虑,但如果你的数据无论如何都不能被移动(例如:一个有大量整数的POD),就没有必要进行额外的复制。 - user90843
2
Mehrdad,不确定您的期望是什么,但代码按预期工作。 - user90843
我认为为了说服编译器类型不重叠而进行复制的必要性是语言上的缺陷。我宁愿使用GCC的__restrict__(也可以用于引用),而不是进行过多的复制。很遗憾,标准C ++没有采用C99的restrict关键字。 - Ruslan

16

视所使用的类型而定。通过传递引用和解引用,会增加一些开销。对于大小等于或小于指针并且使用默认复制构造函数的类型,按值传递可能更快。


2
对于非本地类型,使用const引用而不是普通引用可能会提高性能(取决于编译器优化代码的程度)。 - OJ.

9
这是我在设计非模板函数接口时通常遵循的方式:
  1. 如果函数不想修改参数且值容易复制(int,double,float,char,bool等),则按值传递。注意,std::string,std::vector和标准库中的其他容器都不属于此类。

  2. 如果值很难复制且函数不想修改指向的值,并且NULL是函数处理的值,请通过const指针传递。

  3. 如果值很难复制且函数想修改指向的值,并且NULL是函数处理的值,请通过非const指针传递。

  4. 如果值很难复制且函数不想修改所引用的值,并且如果使用指针,则NULL将不是有效值,请按const引用传递。

  5. 如果值很难复制且函数希望修改所引用的值,并且如果使用指针,则NULL将不是有效值,请按非const引用传递。


1
std::optional引入程序中,您就不再需要使用指针。 - Violet Giraffe

9
正如已经指出的那样,这取决于类型。对于内置数据类型,最好按值传递。即使是一些非常小的结构,比如一对int,通过按值传递也可以更好地执行。
以下是一个例子,假设您有一个整数值,并且您想将其传递给另一个程序。如果该值已经被优化为存储在寄存器中,那么如果您想按引用传递它,则必须首先将其存储在内存中,然后将指向该内存的指针放置在堆栈上以执行调用。如果它是按值传递的,那么所需的只是将寄存器推入堆栈。(由于不同的调用系统和CPU,细节比这个复杂得多)。
如果您正在进行模板编程,通常必须始终通过const ref传递,因为您不知道要传递的类型。通过按值传递错误的内容会导致传递惩罚比通过const ref传递内置类型的惩罚更严重。

5

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


我不确定为什么这个被投票否决了?对我来说很有道理。如果你需要当前存储的值,那么就按值传递。如果不需要,则传递引用。 - Totty
5
这完全取决于数据类型。通过引用传递POD(普通旧数据)类型实际上可能会增加内存访问,从而降低性能。 - Torlack
1
显然按引用传递 int 没有节省什么!我认为这个问题暗示了比指针更大的东西。 - GeekyMonkey
5
这并不是那么明显的,我见过很多人写的代码,他们并不真正理解计算机如何工作,只是因为被告知这是最好的做法,所以通过const ref传递简单的东西。 - Torlack

4

通常情况下,通过const引用传递参数更好。 但是如果您需要在本地修改函数参数,则最好使用按值传递。 对于某些基本类型,无论是按值传递还是按引用传递,性能通常相同。实际上,引用在内部表示为指针,这就是为什么您可以期望例如对于指针,两种传递方式在性能上都是相同的,甚至按值传递可能会更快,因为不需要解除引用。


如果您需要修改被调用函数的参数副本,您可以在被调用代码中制作一个副本而不是按值传递。在我看来,通常不应该根据这样的实现细节选择API:无论哪种方式,调用代码的源代码都是相同的,但其目标代码不同。 - Steve Jessop
如果你通过值传递参数,就会创建一个副本。在我看来,无论你是通过按值传递参数还是本地方式创建副本,这都与C++有关。但从设计角度来看,我同意你的观点。但我在这里只描述C++的特性,不涉及设计。 - sergtk

4

对于小数据类型,采用传值方式。

对于大数据类型(大的定义因机器而异),采用const引用方式传递。但在C++11中,如果你要使用数据,则采用传值方式,因为你可以利用移动语义优化性能。例如:

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

现在调用代码会执行以下操作:

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

只有一个对象会被创建并直接移入到Person类成员name_中。如果你通过const引用传递,那么会需要复制该对象以便放入name_中。


1
通常情况下,对于非类类型的值传递,使用const引用传递类。 如果一个类非常小,最好通过值传递,但差异很小。你真正想避免的是通过值传递一些巨大的类并且它们全部被复制——如果你传递一个有很多元素的std::vector,这将会产生巨大的影响。

我的理解是,std::vector 实际上在堆上分配其项,而向量对象本身永远不会增长。哦,等等。但是,如果操作导致复制向量,则实际上将复制所有元素。那就糟糕了。 - Steven Lu
1
是的,这正是我所想的。sizeof(std::vector<int>)是常量,但如果没有编译器的巧妙处理,在按值传递时仍将复制其内容。 - Peter

-4

按引用传递比按值传递更好。我在Leetcode上解决最长公共子序列问题时,按值传递显示TLE,但按引用传递则接受了代码。花了我30分钟才弄清楚。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接