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


Answers:


203

它曾经被通常建议的最佳实践1由常量REF使用通行证的所有类型,除了内建类型(charintdouble等),用于迭代器和用于功能对象(lambda表达式,类从导出std::*_function)。

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

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

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

在这些情况下,我们需要进行以下(简化)的权衡:

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

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

对于实施适当的移动的构造(如载体,字符串...)大的物体,第二壳体是则大大大于第一更有效。因此,如果函数拥有参数的所有权,并且对象类型支持有效的移动,则建议使用按值传递


历史记录:

实际上,任何现代编译器都应该能够弄清楚何时按值传递很昂贵,并在可能的情况下隐式转换调用以使用const ref。

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

但是总的来说,编译器无法确定这一点,而C ++中移动语义的问世使得这种优化的重要性大大降低。


1例如,Scott Meyers,《有效的C ++》

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


嗯...我不确定是否值得通过裁判。double-s
sergtk

3
像往常一样,增强在这里有帮助。boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm具有模板内容,可以自动确定类型是内置类型的时间(对于模板很有用,在某些情况下您有时不容易知道这一点)。
CesarB

13
这个答案错过了一个重点。为了避免切片,您必须通过引用(const或其他方式)传递。参见stackoverflow.com/questions/274626/…–
克里斯·N

6
@克里斯:对。我忽略了多态的整个部分,因为那是完全不同的语义。我认为OP(在意义上)意味着“按价值”论点的通过。当需要其他语义时,问题甚至不会提出。
Konrad Rudolph

98

编辑: 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的新值。

有关更多实用的规则,请参见“ 移动构造函数”文章中的一组很好的规则(强烈建议阅读)。

  • 如果该函数打算将参数作为副作用进行更改,请通过非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 */
}

但是,如果参数的目的是在参数中写入一些内容,则通过非常量引用将其传递

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

我发现您的规则很好,但是我不确定您谈论的不通过第一部分,因为裁判会加快速度。是的,当然,但没有传递任何东西作为参考,只是优化的根本没有任何意义。如果要更改要传入的堆栈对象,请通过ref进行。如果您不这样做,请按值传递。如果您不想更改它,请将其作为const-ref传递。传递值所带来的优化应该无关紧要,因为您在传递为ref时会获得其他好处。我不明白“想要的速度?” SICE。如果你是要去完成这些,你会按值传递反正运..
chikuba

约翰尼斯:当我阅读这篇文章时,我很喜欢它,但是当我尝试这篇文章时,我很失望。此代码在GCC和MSVC上均失败。我错过了什么,还是在实践中不起作用?
user541686

我不同意我是否同意,如果您仍然想复制一个副本,则应按值(而不是const ref)传递它,然后将其移动。这样看,还有什么更有效的方法,一个副本和一个移动(如果向前传递,甚至可以拥有2个副本),或者仅仅是一个副本?是的,双方都有一些特殊情况,但是如果您的数据仍然无法移动(例如:带有大量整数的POD),则无需额外的副本。
Ion Todirel,2012年

2
Mehrdad,不确定您的期望如何,但是代码按预期运行
Ion Todirel 2012年

我认为有必要进行复制,只是为了使编译器确信类型不会与语言的缺陷重叠。我宁愿使用GCC __restrict__(也可以在参考文献上使用),而不要使用过多的副本。太糟糕的标准C ++没有采用C99的restrict关键字。
Ruslan

12

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


对于非本机类型,您可以(取决于编译器对代码的优化程度)使用const引用而不是引用来提高性能。
OJ。

9

正如已经指出的,它取决于类型。对于内置数据类型,最好按值传递。甚至一些非常小的结构(例如一对整数)也可以通过传递值来实现更好的性能。

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

如果您正在执行模板编程,通常会因为不知道要传入的类型而被迫始终通过const ref进行传递。对于通过值传递不良值的传递惩罚要比通过内置类型的传递惩罚要差得多。由const ref。


关于术语的注释:包含一百万个整数的结构仍然是“ POD类型”。可能您的意思是“对于内置类型,最好按值传递”。
史蒂夫·杰索普

6

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

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

  2. 如果要复制的值非常昂贵并且函数不想修改指向的值并且NULL是函数处理的值,则通过const指针传递。

  3. 如果要复制的值非常昂贵并且函数要修改指向的值并且NULL是函数处理的值,则通过非const指针传递。

  4. 当值的复制成本很高并且该函数不想修改所引用的值并且如果使用指针代替NULL时,则NULL将不是有效值。

  5. 当值复制成本很高且函数要修改所引用的值时,如果要使用指针代替NULL,则NULL将不是有效值。


添加std::optional到图片,您不再需要指针。
紫罗兰色长颈鹿

5

听起来您已经找到答案了。传递价值很昂贵,但是如果需要,可以给您一个副本。


我不确定为什么这被否决了?对于我,这说得通。如果您需要当前存储的值,则按值传递。如果没有,请通过参考。
托蒂

4
它完全取决于类型。通过引用执行POD(普通旧数据)类型实际上可以通过引起更多的内存访问来降低性能。
Torlack

1
显然,通过引用传递int不会节省任何内容!我认为这个问题所隐含的意义大于指针。
GeekyMonkey

4
并不是很明显,我看过很多人的代码,他们不真正理解计算机如何通过const ref传递简单的事情,因为有人告诉他们这是最好的事情。
Torlack

4

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


如果需要修改被调用方的参数副本,则可以在被调用的代码中进行副本,而不是按值传递。IMO通常,您不应该基于这样的实现细节来选择API:调用代码的源是相同的,但目标代码却不是。
史蒂夫·杰索普

如果按值传递,则会创建副本。而且IMO不管用哪种方式创建副本:通过按值传递参数或在本地传递参数-这就是C ++所关心的。但是从设计的角度来看,我同意你的看法。但是,我仅在此处描述C ++功能,而不涉及设计。
sergtk

1

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


我的理解是std::vector实际上将其项分配在堆上,而矢量对象本身永远不会增长。等一下。但是,如果该操作导致复制矢量,则实际上它将复制所有元素。不好
史蒂文·卢

1
是的,这就是我的想法。sizeof(std::vector<int>)是常量,但是在没有任何编译器聪明的情况下,按值传递它仍将复制内容。
彼得

1

按值传递小类型。

通过const引用传递大类型(big的定义在机器之间可能会有所不同),但是在C ++ 11中,如果要使用数据,则按值传递,因为可以利用移动语义。例如:

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

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

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

并且仅创建一个对象并将其直接移到name_类中的member 中Person。如果您通过const引用传递,则必须将其放入副本name_


-5

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

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

输入参数:account,存款输出参数:总计

输入和输出是不同的使用方式

  1. void amount(int total , int deposit )

投入总额存款产出总额

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.