有效使用移动语义和 (N)RVO
Efficient use of move semantics together with (N)RVO
假设我想实现一个应该处理对象并返回一个新的(可能更改的(对象的函数。我想在 C+11 中尽可能高效地做到这一点。环境如下:
class Object {
/* Implementation of Object */
Object & makeChanges();
};
我想到的替代方案是:
// First alternative:
Object process1(Object arg) { return arg.makeChanges(); }
// Second alternative:
Object process2(Object const & arg) { return Object(arg).makeChanges(); }
Object process2(Object && arg) { return std::move(arg.makeChanges()); }
// Third alternative:
Object process3(Object const & arg) {
Object retObj = arg; retObj.makeChanges(); return retObj;
}
Object process3(Object && arg) { std::move(return arg.makeChanges()); }
注意:我想使用像 process()
这样的包装函数,因为它会做一些其他工作,并且我希望尽可能多地重用代码。
更新:
我使用了带有给定签名的makeChanges()
,因为我正在处理的对象提供了具有该类型签名的方法。我猜他们将其用于方法链接。我还修复了提到的两个语法错误。感谢您指出这些。我还添加了第三种选择,我将在下面提出问题。
用叮当尝试这些[即 Object obj2 = process(obj);
] 的结果如下:
第一个选项对复制构造函数进行两次调用;一个用于传递参数,另一个用于返回。可以改为说return std::move(..)
,对复制构造函数进行一次调用,对移动构造函数进行一次调用。我知道 RVO 无法摆脱这些调用之一,因为我们正在处理函数参数。
在第二个选项中,我们仍然有两个对复制构造函数的调用。在这里,我们进行一次显式调用,并在返回时进行一次调用。我期待 RVO 启动并摆脱后者,因为我们返回的对象与参数不同。然而,它并没有发生。
在第三个选项中,我们只有一个对复制构造函数的调用,这是显式调用。(N(RVO 消除了我们为返回而进行的复制构造函数调用。
我的问题如下:
- (回答(为什么 RVO 启动最后一个选项而不是第二个选项?
- 有没有更好的方法可以做到这一点?
- 如果我们传入临时选项,则第 2 个和第 3 个选项将在返回时调用移动构造函数。是否可以使用 (N(RVO 消除它?
谢谢!
测量,所以我设置了这个Object
:
#include <iostream>
struct Object
{
Object() {}
Object(const Object&) {std::cout << "Object(const Object&)n";}
Object(Object&&) {std::cout << "Object(Object&&)n";}
Object& makeChanges() {return *this;}
};
我推测,一些解决方案可能会对x值和prvalue(两者都是rvalue(给出不同的答案。 所以我决定测试它们(除了左值(:
Object source() {return Object();}
int main()
{
std::cout << "process lvalue:nn";
Object x;
Object t = process(x);
std::cout << "nprocess xvalue:nn";
Object u = process(std::move(x));
std::cout << "nprocess prvalue:nn";
Object v = process(source());
}
现在很简单,就是尝试你所有的可能性,那些别人贡献的可能性,我自己扔了一个:
#if PROCESS == 1
Object
process(Object arg)
{
return arg.makeChanges();
}
#elif PROCESS == 2
Object
process(const Object& arg)
{
return Object(arg).makeChanges();
}
Object
process(Object&& arg)
{
return std::move(arg.makeChanges());
}
#elif PROCESS == 3
Object
process(const Object& arg)
{
Object retObj = arg;
retObj.makeChanges();
return retObj;
}
Object
process(Object&& arg)
{
return std::move(arg.makeChanges());
}
#elif PROCESS == 4
Object
process(Object arg)
{
return std::move(arg.makeChanges());
}
#elif PROCESS == 5
Object
process(Object arg)
{
arg.makeChanges();
return arg;
}
#endif
下表总结了我的结果(使用 clang -std=c++11(。 第一个数字是复制构造的数量,第二个数字是移动构造的数量:
+----+--------+--------+---------+
| | lvalue | xvalue | prvalue | legend: copies/moves
+----+--------+--------+---------+
| p1 | 2/0 | 1/1 | 1/0 |
+----+--------+--------+---------+
| p2 | 2/0 | 0/1 | 0/1 |
+----+--------+--------+---------+
| p3 | 1/0 | 0/1 | 0/1 |
+----+--------+--------+---------+
| p4 | 1/1 | 0/2 | 0/1 |
+----+--------+--------+---------+
| p5 | 1/1 | 0/2 | 0/1 |
+----+--------+--------+---------+
对我来说,process3
似乎是最好的解决方案。 但是,它确实需要两个重载。 一个用于处理左值,一个用于处理右值。 如果由于某种原因这有问题,则解决方案 4 和 5 只需一个重载即可完成工作,但代价是 glvalue(左值和 x值(的额外移动构造为 1 个。 这是一个判断电话,一个人是否愿意支付额外的移动结构来节省过载(没有一个正确的答案(。
(回答(为什么 RVO 启动最后一个选项而不是第二个选项?
要启动 RVO,返回语句需要如下所示:
return arg;
如果您将其复杂化:
return std::move(arg);
或:
return arg.makeChanges();
然后 RVO 被抑制。
有没有更好的方法可以做到这一点?
我最喜欢的是p3和p5。 我对p5的偏好而不是p4仅仅是风格上的。 当我知道它会自动应用时,我回避将move
放在return
语句上,因为害怕意外抑制 RVO。 然而,在 p5 中,RVO 无论如何都不是一个选项,即使 return 语句确实得到了隐式移动。 所以 p5 和 p4 确实是等价的。 选择您的风格。
">如果我们暂时通过,第 2 和第 3 选项将调用移动 返回时的构造函数。可以使用消除它 (N(房车?
prvalue"列与"xvalue"列解决了这个问题。 有些解决方案为 xvalues 添加了额外的移动构造,有些则没有。
您显示的任何函数都不会对其返回值进行任何重要的返回值优化。
makeChanges
返回一个Object&
。因此,必须将其复制到值中,因为您要返回它。因此,前两个将始终复制要返回的值。就副本数而言,第一个副本制作两个副本(一个用于参数,一个用于返回值(。第二个创建两个副本(一个在函数中显式复制,一个用于返回值。
第三个甚至不应该编译,因为你不能隐式地将 l 值引用转换为 r 值引用。
所以真的,不要这样做。如果你想传递一个对象,并在原地修改它,那么只需这样做:
Object &process1(Object &arg) { return arg.makeChanges(); }
这将修改提供的对象。没有复制或任何东西。当然,人们可能想知道为什么process1
不是成员函数或其他东西,但这并不重要。
最快的方法是 - 如果参数是左值,则复制它并返回该副本 - 如果右值,则移动它。退货始终可以移动或应用 RVO/NRVO。这很容易实现。
Object process1(Object arg) {
return std::move(arg.makeChanges());
}
这与许多运算符重载的规范 C++11 形式非常相似。
- 何时在引用或唯一指针上使用移动语义
- 如何从具有移动语义的类对象中生成共享指针
- Boost Spirit,获取迭代器内部语义动作
- 可以使用移动语义更改或改进此C++代码吗?
- c++在使用指针时移动语义
- 在C++17中,引用const字符串的语义应该是什么
- Xcode 语义问题引用或以前定义的代码
- 使用移动和复制语义时函数匹配如何工作?
- 在 c++ 中,如何返回多个对象并从 RVO 中受益
- 将向量从 N1 缩小到 N2 项,而不触发默认构造函数并仅使用 move 语义
- 在 RAII 构造中修改 RVO 值是否安全?
- 移动语义和深层/浅层复制之间有什么关系?
- 了解构造函数在移动、复制、赋值语义中的行为
- std::unique_lock移动语义
- 移动语义和运算符 + 重载
- C++ 移动语义是否在任何情况下都能节省资源?
- 当 RVO 可以应用时,为什么要按shared_ptr而不是按值返回?
- RVO,移动语义和争取最佳代码
- 有效使用移动语义和 (N)RVO
- C++11编译器何时会使RVO和NRVO优于移动语义和常量引用绑定