这个问题是由C++11中对RVO的混淆引发的。
我有两种"返回"值的方法:按值返回和通过引用参数返回 这是我的示例代码,这两个代码做相同的工作: 按值返回 通过参考参数返回 以下是我的问题: 更新多亏了这些答案,我知道第一种方法在大多数方面都更好。 这里有一些有用的相关链接,可以帮助我理解这个问题:struct SolutionType
{
vector<double> X;
vector<double> Y;
SolutionType(int N) : X(N),Y(N) { }
};
SolutionType firstReturnMethod(const double input1,
const double input2);
{
// Some work is here
SolutionType tmp_solution(N);
// since the name is too long, I make alias.
vector<double> &x = tmp_solution.X;
vector<double> &y = tmp_solution.Y;
for (...)
{
// some operation about x and y
// after that these two vectors become very large
}
return tmp_solution;
}
void secondReturnMethod(SolutionType& solution,
const double input1,
const double input2);
{
// Some work is here
// since the name is too long, I make alias.
vector<double> &x = solution.X;
vector<double> &y = solution.Y;
for (...)
{
// some operation about x and y
// after that these two vectors become very large
}
}
首先,对您正在做的事情的适当技术术语是NRVO。RVO与被退回的临时物品有关:
X foo() {
return make_x();
}
NRVO是指返回的命名对象:
X foo() {
X x = make_x();
x.do_stuff();
return x;
}
其次,(N)RVO是编译器优化,并不是强制性的。然而,您可以非常确信,如果您使用现代编译器,(N)RVO将被非常积极地使用。
第三,(N)RVO是而不是C++11功能-早在2011年之前就已经存在了。
首先,C++11中有一个move构造函数。因此,如果您的类支持移动语义,那么即使(N)RVO没有发生,它也将从中移动,而不是复制。不幸的是,并不是所有的东西都能在语义上有效地移动。
第五,引用返回是一个可怕的反模式。它确保对象将被有效地创建两次——第一次是作为"空"对象,第二次是在填充数据时——并且它阻止您使用"空"状态不是有效不变量的对象。
SergyA的答案是完美的。如果你听从这个建议,你几乎总是不会出错的。
然而,有一种"结果",最好从调用站点传递对结果的引用。
这是在使用std
容器作为循环中的结果缓冲区的情况下发生的。
如果你看一下函数std::getline
,你会看到一个例子。
CCD_ 3被设计为从输入流填充CCD_。
每次使用相同的字符串引用调用getline时,都会覆盖字符串的数据。请注意,随着时间的推移(假设随机行长度),有时需要字符串的隐式reserve
才能容纳新的长行。然而,比迄今为止最长的线路更短的线路将不需要reserve
,因为已经有足够的capacity
。
想象一个具有以下签名的getline版本:
std::string fictional_getline(std::istream&);
这意味着每次调用函数时都会返回一个新字符串。无论是否发生RVO或NRVO,都需要创建该字符串,如果它比短字符串优化边界长,则需要分配内存。此外,字符串的内存在每次超出作用域时都会被释放。
在这种情况下,以及其他类似情况下,将结果容器作为引用传递会更加高效。
示例:
void do_processing(const std::string& s)
{
// ...
}
/// @post: in the case of an error, os.bad() == true
/// @post: in the case of no error, os.bad() == false
std::string fictional_getline(std::istream& stream)
{
std::string result;
if (not std::getline(stream, result))
{
// what to do here?
}
return result;
}
// note that buf is re-used which will require fewer and fewer
// reallocations the more the loop progresses
void fast_process(std::istream& stream)
{
std::string buf;
while(std::getline(std::cin, buf))
{
do_processing(buf);
}
}
// note that buf is re-created and destroyed each time around the loop
void not_so_fast_process(std::istream& stream)
{
for(;;)
{
auto buf = fictional_getline(stream);
if (!stream) break;
do_processing(buf);
}
}
无法确保RVO(或NVRO)发生在C++11中。无论它是否发生,它都与实现质量(例如编译器)有关,而不是由程序员从根本上控制。
移动语义在某些情况下可以用来实现类似的效果,但与RVO不同。
一般来说,我建议使用任何适用于手头数据的返回方法,这是程序员可以理解的。程序员能够理解的代码更容易正确工作。使用晦涩难懂的技术来优化性能(例如,试图迫使NVRO发生)往往会使代码更难理解,因此更容易出错(例如,增加未定义行为的可能性)。如果代码工作正常,但MEASUREMENT显示它缺乏所需的性能,那么可以探索更神秘的技术来提高性能。但是,试图提前(即在任何测量提供需求证据之前)精心手工优化代码被称为"过早优化"是有原因的。
通过引用返回可以避免在函数返回时复制大数据。因此,如果函数返回一个大型数据结构,则通过引用返回可能比通过值返回更有效(通过各种措施)。不过,这是有权衡的-如果基础数据不存在,而其他代码有对它的引用,那么返回对某个东西的引用是危险的(导致未定义的行为)。然而,返回值会使一些代码很难保存对(例如)可能已不存在的数据结构的引用。
编辑:根据注释中的要求,添加引用返回是危险的示例。
AnyType &func()
{
Anytype x;
// initialise x in some way
return x;
};
int main()
{
// assume AnyType can be sent to an ostream this wah
std::cout << func() << 'n'; // undefined behaviour here
}
在这种情况下,func()
返回一个引用,该引用在返回后不再存在,通常称为悬空引用。因此,该引用的任何使用(在本例中,打印引用的值)都具有未定义的行为。按值返回(即简单地删除&
)会返回变量的副本,当调用方尝试使用该副本时,该副本就会存在
未定义行为的原因是func()
的返回方式。但是,未定义的行为将发生在调用方(使用引用)中,而不是func()
本身。因果之间的分离可能会使bug很难追踪。