我想知道哪种方式更好的实践方法。我认识到通过引用传递(加上const)可以提供更好的程序性能,因为你不需要复制变量。
曾经普遍建议最佳实践是对于除了内置类型(char
、int
、double
等)、迭代器和函数对象(lambda、从std::*_function
派生的类)之外的所有类型都使用常量引用传递。1
在存在移动语义之前,这尤其正确。原因很简单:如果你通过值传递,那么必须复制该对象,除非是非常小的对象,否则始终比传递引用更昂贵。
有了C++11,我们获得了移动语义。简而言之,移动语义允许在某些情况下“按值”传递对象而不复制它。特别地,当您要传递的对象是一个rvalue时就是这种情况。
本质上,移动对象仍然至少与通过引用传递一样昂贵。但是,在许多情况下,函数将在内部复制对象——即它将拥有该参数。2
在这些情况下,我们有以下(简化的)权衡:
历史注释:
事实上,任何现代编译器都应该能够判断传值调用是否昂贵,并在可能的情况下自动将调用转换为使用const引用。
理论上而言。 实际上,编译器不能总是更改此操作而不破坏函数的二进制接口。在某些特殊情况下(当函数被内联时),如果编译器可以确定原始对象不会通过函数中的操作发生更改,则拷贝实际上将被省略。
但是通常情况下,编译器无法确定这一点,C++中移动语义的出现使得这种优化变得不那么相关了。
1 例如,在Scott Meyers的Effective C++中。
2 这在对象构造函数中尤其常见,它们可能会接受参数并将它们存储在内部,以成为构建对象状态的一部分。
std::move
)。 - Konrad Rudolphconst
引用传递应该是非常罕见的:大多数函数不应直接修改它们的参数,而是返回修改后的值作为副本。这通常会导致更安全、更易于维护的代码(当然,出于性能原因也有例外)。 - Konrad Rudolph对于结构体,如果复制成本较低,则通过值传递的额外优点是编译器可以假设对象不是别名(不是相同的对象)。使用按引用传递时,编译器不能总是这样假设。以下是一个简单的例子:
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中,可以使用语言支持来实现这种技术。在那之前,我会遵循上述规则。
示例:要将字符串转换为大写并返回大写版本,应始终通过值传递:必须制作它的副本(不能直接更改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 */
}
__restrict__
(也可以用于引用),而不是进行过多的复制。很遗憾,标准C ++没有采用C99的restrict
关键字。 - Ruslan视所使用的类型而定。通过传递引用和解引用,会增加一些开销。对于大小等于或小于指针并且使用默认复制构造函数的类型,按值传递可能更快。
如果函数不想修改参数且值容易复制(int,double,float,char,bool等),则按值传递。注意,std::string,std::vector和标准库中的其他容器都不属于此类。
如果值很难复制且函数不想修改指向的值,并且NULL是函数处理的值,请通过const指针传递。
如果值很难复制且函数想修改指向的值,并且NULL是函数处理的值,请通过非const指针传递。
如果值很难复制且函数不想修改所引用的值,并且如果使用指针,则NULL将不是有效值,请按const引用传递。
如果值很难复制且函数希望修改所引用的值,并且如果使用指针,则NULL将不是有效值,请按非const引用传递。
std::optional
引入程序中,您就不再需要使用指针。 - Violet Giraffe听起来你已经得到了答案。传递值是昂贵的,但如果需要,它会给你一个副本来使用。
通常情况下,通过const引用传递参数更好。 但是如果您需要在本地修改函数参数,则最好使用按值传递。 对于某些基本类型,无论是按值传递还是按引用传递,性能通常相同。实际上,引用在内部表示为指针,这就是为什么您可以期望例如对于指针,两种传递方式在性能上都是相同的,甚至按值传递可能会更快,因为不需要解除引用。
对于小数据类型,采用传值方式。
对于大数据类型(大的定义因机器而异),采用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_
中。
std::vector
实际上在堆上分配其项,而向量对象本身永远不会增长。哦,等等。但是,如果操作导致复制向量,则实际上将复制所有元素。那就糟糕了。 - Steven Lusizeof(std::vector<int>)
是常量,但如果没有编译器的巧妙处理,在按值传递时仍将复制其内容。 - Peter按引用传递比按值传递更好。我在Leetcode上解决最长公共子序列问题时,按值传递显示TLE,但按引用传递则接受了代码。花了我30分钟才弄清楚。