从C ++中的函数返回值时,我们具有复制省略和(命名)返回值优化,可帮助我们创建更有效的代码。简而言之,下面的代码:
std::vector<int> make_vec_1(){
std::vector<int> v;
v.resize(1e6);
return v;
}
导致静默移动或直接构造到返回值的目的地,而不是副本。与此相关的规则还意味着,在返回时显式移动返回的对象实际上会阻止这些优化。
std::vector<int> make_vec_2(){
std::vector<int> v;
v.resize(1e6);
return std::move(v); // BAD
}
此版本可防止RVO,如Scott Meyers的 Effective Modern C ++ (条款25)中所述。
我的问题是,当返回类型不同但可以从一个或多个局部变量进行移动构造时会发生什么?考虑以下每个返回可选向量的函数:
std::optional<std::vector<int>> make_opt_vec_1(){
std::vector<int> v;
v.resize(1e6);
return v; // no move
}
std::optional<std::vector<int>> make_opt_vec_2(){
std::vector<int> v;
v.resize(1e6);
return std::move(v); // move
}
其中哪些是正确的?起初,return std::move(v)
行对我来说似乎是一个危险信号,但我也怀疑这是正确的做法。以下两个函数返回一对向量也是如此:
std::pair<std::vector<int>, std::vector<int>> make_vec_pair_1(){
std::vector<int> v1, v2;
v1.resize(1e6);
v2.resize(1e6);
return {v1, v2}; // no move
}
std::pair<std::vector<int>, std::vector<int>> make_vec_pair_2(){
std::vector<int> v1, v2;
v1.resize(1e6);
v2.resize(1e6);
return {std::move(v1), std::move(v2)}; // move
}
在这种情况下,尽管乍看起来看起来很怪异,但我认为进入返回值是更好的选择。
我是正确的,当类型不同时最好移回返回值,但是可以从要从中移出的局部变量构造返回值吗?我是否误解了NRVO,或者还有其他优化在我前面吗?
答案 0 :(得分:9)
我是正确的,当类型不同时最好移回返回值,但是可以从要从中移出的局部变量构造返回值吗?我是否误解了NRVO,或者还有其他优化在我前面吗?
您确实错过了一件事。即使类型不同,也会自动进行隐式移动。
[class.copy.elision] (强调我的意思)
3在以下复制初始化上下文中,移动操作 可能会代替复制操作:
首先执行
如果return语句中的表达式是一个(可能带有括号的)id表达式,该表达式使用自动命名对象 正文或参数声明子句中声明的存储期限 最里面的封闭函数或lambda-expression或
如果throw-expression的操作数是非易失性自动对象的名称(函数或catch子句参数除外) 其范围不超出最内层封闭的范围 try-block(如果有),
重载分辨率以选择副本的构造函数,就好像该对象是由右值指定的一样。如果 第一次重载解析失败或未执行,或者类型 所选构造函数的第一个参数的值不是右值 引用对象的类型(可能是cv限定),重载 再次执行解析,将对象视为左值。 [注意:必须执行此两阶段过载解析 不管是否会出现复制省略。它决定了 如果未执行省略,则调用构造函数,然后选择 即使调用被取消,构造方法也必须可访问。 - 结束 注意]
这不取决于类型匹配,并且是回退行为,以防不发生完整(N)RVO的情况。因此,通过显式移至make_opt_vec_2
,您一无所获。
考虑到std::move
要么是悲观的,要么完全是多余的,我认为最好在仅返回函数局部对象的情况下做到这一点。
唯一要明确编写移动的情况是返回的表达式更复杂时。在那种情况下,您确实是一个人,不动则是潜在的悲观情绪。因此,在make_vec_pair_2
中,将对移到 是正确的事情。
这里的经验法则是不要仅移动作为函数本地对象的id表达式。否则,请移开。