这个问题是由C ++ 11中关于RVO的混淆引发的。
我有两种方法可以“返回”值:按值返回,通过参考参数返回。如果我不考虑性能,我更喜欢第一个。由于按值返回更自然,我可以轻松区分输入和输出。但是,如果我在回归大数据时考虑效率。我无法决定,因为在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
}
}
以下是我的问题:
更新 感谢这些答案,我知道第一种方法在大多数方面都更好。
以下是一些有用的相关链接,可以帮助我理解这个问题:
答案 0 :(得分:16)
首先,您所做的正确技术术语是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没有发生,它也将被移动而不是被复制。不幸的是,并非一切都可以在语义上有效地移动。
第五,通过引用返回是一个可怕的反模式。它确保对象有效地创建两次 - 第一次是“空”&#39;对象,第二次填充数据时 - 它会阻止您使用“清空”的对象。 state不是有效的不变量。
答案 1 :(得分:3)
SergyA的答案是完美的。如果你遵循这个建议,你几乎总是不会出错。
然而,有一种“结果”,最好从调用网站传递对结果的引用。
在您使用std
容器作为循环中的结果缓冲区的情况下。
如果您查看函数std::getline
,您会看到一个示例。
std::getline
旨在从输入流中填充std::string
缓冲区。
每次使用相同的字符串引用调用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);
}
}
答案 2 :(得分:1)
无法确保在C ++ 11中发生RVO(或NVRO)。无论是否发生,它都与实现的质量(例如编译器)有关,而不是程序员可以从根本上控制的。
在某些情况下可以使用移动语义来实现类似的效果,但与RVO不同。
通常,我建议使用适用于手头数据的任何返回方法,这是程序员可以理解的。程序员可以理解的代码更容易正常工作。使用奥术技术来优化性能(例如,试图迫使NVRO发生)倾向于使代码更难以理解,因此更可能有错误(例如,增加未定义行为的可能性)。如果代码工作正常,但MEASUREMENTS表明它缺乏必要的性能,那么可以探索更多神秘的技术来提高性能。但是,尝试亲自优化手动优化代码(即在任何测量提供需要的证据之前)被称为“过早优化”是有原因的。
通过引用返回允许避免在函数返回时复制大数据。因此,如果函数返回大型数据结构,则通过引用返回可以比通过值返回更有效(通过各种度量)。虽然有一些权衡 - 如果基础数据不再存在而某些其他代码引用它,则返回对某事物的引用是危险的(导致未定义的行为)。然而,返回一个值会使某些代码难以保存对(例如)可能已不复存在的数据结构的引用。
编辑:添加示例,其中按引用返回是危险的,如评论中所要求的那样。
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()
本身。原因和结果之间的分离可能会导致很难追查的错误。