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

Is it better in C++ to pass by value or pass by reference-to-const?

提问人:Matt Pascoe 提问时间:11/7/2008 最后编辑:claymationMatt Pascoe 更新时间:6/12/2022 访问量:124415

问:

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

我想知道哪种做法更好。我意识到通过引用 const 应该在程序中提供更好的性能,因为您没有复制变量。

C++ 变量 引用 常量 按值传递

评论

0赞 sbi 9/27/2011
相关新闻: stackoverflow.com/questions/2139224/...

答:

5赞 GeekyMonkey 11/7/2008 #1

听起来你得到了答案。按值传递很昂贵,但如果需要,可以为您提供一个副本。

评论

0赞 Totty 11/7/2008
我不确定为什么这被否决了?这对我来说很有意义。如果需要当前存储的值,请按值传递。如果没有,请传递引用。
5赞 Torlack 11/7/2008
它完全取决于类型。实际上,通过引用执行 POD(纯旧数据)类型可能会通过导致更多的内存访问来降低性能。
1赞 GeekyMonkey 11/7/2008
显然,通过引用传递 int 并不能保存任何东西!我认为这个问题意味着比指针更重要的事情。
5赞 Torlack 11/7/2008
这并不是那么明显,我见过很多不真正了解计算机如何工作的人的代码,他们通过 const ref 传递简单的东西,因为他们被告知这是最好的做法。
16赞 Lou Franco 11/7/2008 #2

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

评论

2赞 OJ. 11/7/2008
对于非本机类型,您可能会(取决于编译器优化代码的程度)使用 const 引用而不仅仅是引用来提高性能。
236赞 Konrad Rudolph 11/7/2008 #3

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

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

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

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

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

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

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

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


历史笔记:

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

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

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


1 例如,在斯科特·迈耶斯(Scott Meyers)中,有效的C++

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

评论

4赞 CesarB 11/7/2008
像往常一样,boost在这里有所帮助。boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm 有模板的东西,可以自动确定一个类型何时是内置类型(对于模板很有用,你有时不容易知道)。
18赞 ChrisN 11/16/2008
这个答案忽略了一个重要的点。为了避免切片,必须通过引用(const 或其他方式)传递。查看 stackoverflow.com/questions/274626/...
8赞 Konrad Rudolph 11/16/2008
@Chris:对。我省略了多态性的整个部分,因为这是一个完全不同的语义。我相信 OP(语义上)的意思是“按值”参数传递。当需要其他语义时,问题甚至不会自行提出。
1赞 Konrad Rudolph 9/7/2022
@palapapa 在本段的上下文中,“取得所有权”并不意味着从另一个名称中夺走所有权,而只是接管它,即事后拥有资源。重要的一点不是资源(副本)是否在其他地方拥有,而仅仅是函数是否需要所有权。当然,你是对的,按值传递在逻辑上会创建一个副本。但是,如果论点是暂时的,则不会进行实际复制;相反,该值将被移动。如果该值不是临时值,则无论如何都不能使用右值引用(除非调用方使用 )。std::move
1赞 Konrad Rudolph 1/20/2023
@experimentunit1998X 是的,当然。事实上,在现代 C++ 中,通过非引用传递应该是非常罕见的:大多数函数不应该直接修改它们的参数,而是将修改后的值作为副本返回。这通常会导致更安全、更易于维护的代码(当然,出于性能原因也有例外)。const
4赞 sergtk 11/7/2008 #4

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

评论

0赞 Steve Jessop 11/7/2008
如果需要修改被调用方的参数副本,可以在被调用的代码中创建副本,而不是按值传递。IMO 您通常不应该根据这样的实现细节来选择 API:无论哪种方式,调用代码的源代码都是相同的,但其目标代码不是。
0赞 sergtk 11/8/2008
如果按值传递,则创建副本。和 IMO 无论您以哪种方式创建副本:通过参数传递值或本地 - 这就是 C++ 的问题。但从设计的角度来看,我同意你的看法。但我在这里只描述C++功能,不涉及设计。
9赞 Torlack 11/7/2008 #5

如前所述,这取决于类型。对于内置数据类型,最好按值传递。即使是一些非常小的结构,例如一对整数,也可以通过传递值来表现更好。

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

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

1赞 Peter 11/7/2008 #6

根据经验,非类类型的值和类的常量引用。 如果一个类真的很小,那么按值传递可能更好,但差异很小。你真正想要避免的是按值传递一些巨大的类,并让它全部复制 - 如果你传递,比如说,一个包含很多元素的 std::vector,这将产生巨大的差异。

评论

0赞 Steven Lu 1/26/2012
我的理解是,实际上在堆上分配其项目,而向量对象本身永远不会增长。哦,等等。但是,如果该操作导致复制向量,它实际上会复制所有元素。那会很糟糕。std::vector
1赞 Peter 1/31/2012
是的,我就是这么想的。 是常量,但按值传递它仍然会在没有任何编译器智能的情况下复制内容。sizeof(std::vector<int>)
104赞 Johannes Schaub - litb 11/7/2008 #7

编辑:戴夫·亚伯拉罕斯(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 一文中找到(强烈推荐阅读)。

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

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

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

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 */
}

评论

0赞 chikuba 5/2/2012
我发现你的规则很好,但我不确定第一部分,你说不把它作为参考传递会加快速度。是的,当然,但是不传递某些东西作为参考只是优化的 cus 根本没有意义。如果要更改要传入的堆栈对象,请通过 ref 执行此操作,如果不更改,请按值传递。如果您不想更改它,请将其作为 const-ref 传递。传递值带来的优化应该无关紧要,因为您在作为引用传递时会获得其他东西,我不明白“想要速度”,如果您要在哪里执行这些操作,无论如何您都会按值传递。.
0赞 user541686 8/15/2012
Johannes:当我读到那篇文章时,我很喜欢它,但当我尝试它时,我感到很失望。此代码在 GCC 和 MSVC 上都失败。我是否错过了什么,或者它在实践中不起作用?
0赞 10/14/2012
我不认为我同意,如果你想无论如何制作一个副本,你会按值传递它(而不是 const ref),然后移动它。这样看,一个副本和一个移动(如果你把它传递下去,你甚至可以有 2 个副本),还是只是一个副本更有效率?是的,任何一方都有一些特殊情况,但是如果您的数据无论如何都无法移动(例如:具有大量整数的 POD),则不需要额外的副本。
2赞 10/14/2012
Mehrdad,不确定您的期望,但代码按预期工作
0赞 Ruslan 7/9/2017
我认为复制的必要性只是为了让编译器相信这些类型不会与语言中的缺陷重叠。我宁愿使用 GCC(也可以处理引用)而不是做过多的副本。太糟糕了,标准C++没有采用C99的关键字。__restrict__restrict
4赞 Germán Diago 10/29/2013 #8

小类型的按值传递。

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

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

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

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

并且只会创建一个对象并直接移动到类中的成员中。如果通过 const 引用,则必须制作一份副本才能将其放入 .name_Personname_

-6赞 Dhirendra Sengar 1/5/2015 #9

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

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

输入参数:账户、存款 输出参数:总计

输入和输出是不同的,使用Vaule调用

  1. void amount(int total , int deposit )

输入存款总额 总产量

9赞 Martin G 6/11/2015 #10

这是我在设计非模板函数的界面时通常采用的方法:

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

  2. 如果该值的复制成本很高,则通过常量指针传递,而函数确实如此 不想修改指向的值,NULL 是函数处理的值。

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

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

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

评论

1赞 Violet Giraffe 1/16/2020
添加到图片中,您不再需要指针。std::optional
-4赞 aaa 4/24/2021 #11

Pass by referece 比 pass by value 更好。我正在解决 Leetcode 上最长的常见子序列问题。它显示 TLE 的传递值,但接受代码的引用传递。我花了 30 分钟才弄清楚这一点。