问题描述
我不太熟悉移动在 C++ 中的工作原理,我需要一些帮助来澄清我的理解。我想重载 operator+,我有几个关于它的问题。
ap_n operator+(const ap_n& n,const ap_n& m) {
ap_n tmp {n};
return tmp += m;
}
我的第一个问题是如何使临时对象可移动。如我上面的函数所示,这两个参数都不会改变,因此我需要创建第三个对象来执行操作。
如何使我的返回值可用于移动操作。返回值是否应该是作为 ap_n&
的引用?返回对象应该用std::move(tmp)
封装吗?还是就这样?
C++ 如何决定一个对象何时是右值,或者决定一个对象是否适合进行移动操作,以及如何告诉程序一个对象可以安全地用于移动操作。
ap_n operator+(const ap_n&,const ap_n&); // defined as in first question
ap_n operator+(ap_n&& n,const ap_n& m) { return n += m; }
ap_n operator+(const ap_n& n,ap_n&& m) { return m += n; }
ap_n operator+(ap_n&& n,ap_n&& m) { return n += m; }
我的第二个问题是是否有必要创建接受右值参数的函数变体。现在我有 4 个函数,如图所示,能够接受普通对象和右值对象。
是否有必要像这样写出所有可能的组合?如果我去掉第一个函数以外的所有函数,程序还能正确执行移动操作吗?
解决方法
作为调试技巧,可以帮助正确处理这些事情的是在移动构造函数中打印一条消息
ap_n(ap_n&& o): x_(std::move(o.x_)) { std::cerr << "Move constructed\n"; }
加上其他构造函数和析构函数中的类似消息。然后,您可以清楚地了解创建和销毁实例的时间和方式。
如何使我的返回值可用于移动操作。返回值是否应该是 ap_n& 的引用?返回对象是否应该被 std::move(tmp) 封装?还是就这样?
按值返回结果。 (不要返回对局部变量的引用,因为局部变量会立即超出范围,使引用无效。)您可能会发现这篇简短的文章很有用:Tip of the Week #77: Temporaries,Moves,and Copies。如需更多深度,请查看cppreference on copy elision。
,如何使临时对象可移动
通过为您的类型定义移动构造函数/赋值。
返回值是否应该是 ap_n& 的引用?
返回新对象时不适用于 operator+
。
operator +=
另一方面返回对 lhs 的引用,因此返回 ap_n&
。
如何使我的返回值可用于移动操作。返回对象是否应该被 std::move(tmp) 封装?还是就这样?
来自return statement, 直接返回局部变量时会自动移动。
所以 return tmp;
就足够了。
return std::move(tmp);
阻止 NRVO
return tmp += m;
进行复制,因为您不会“直接”返回 tmp
。
你应该这样做:
ap_n operator+(const ap_n& n,const ap_n& m) {
ap_n tmp {n};
tmp += m;
return tmp; // NRVO,or automatic move
}
return std::move(tmp += m);
会阻止 NRVO,并采取行动。
C++ 如何判断一个对象何时是右值,
粗略地说,
- 变量是左值,因为有名字。
- 返回左值引用的函数 (
ap_n&
) 返回左值。 - 函数返回 r 值引用 (
ap_n&&
),或按值 (ap_n
) 返回 r 值。
或者决定一个移动操作适合一个对象,我如何告诉程序一个对象可以安全地用于移动操作。
重载解析选择有效候选之间的最佳匹配。
所以它需要按值或按右值引用(或转发引用)取函数。
我的第二个问题是
似乎不是第二个;-)
是否有必要创建接受右值参数的函数变体。现在我有 4 个函数,如图所示,能够接受普通对象和右值对象。
是否有必要像这样写出所有可能的组合?
在一般情况下,通过常量引用或值获取单个函数就足够了, 除非你想要优化。所以主要用于库编写器或关键代码。
请注意,您的重载应该被重写以有效地执行移动操作(以重用输入临时参数):
ap_n operator+(ap_n&& n,const ap_n& m)
{
n += m;
return std::move(n); // C++11; C++14,C++17
return n; // C++20
}
或
ap_n&& operator+(ap_n&& n,const ap_n& m)
{
return std::move(n += m);
}
如果我删除除第一个函数以外的所有函数,程序还能正确执行移动操作吗?
与
ap_n operator+(const ap_n& n,const ap_n& m) {
ap_n tmp {n}; // copy
tmp += m;
return tmp; // NRVO,or automatic move
}
对于任何类型的参数,您有 1 个副本,一个 NRVO/move。
与
ap_n&& operator+(ap_n&& n,const ap_n& m) {
return std::move(n += m);
}
你没有动作,但你应该注意引用的生命周期,因为
auto ok = ap_n() + ap_n(); // 1 extra move
auto&& dangling = ap_n() + ap_n(); // No move,but no lifetime extension...
与
ap_n operator+(ap_n&& n,const ap_n& m) {
n += m;
return std::move(n); // C++11,C++14,C++17 // move
return n; // C++20 // automatic move
}
你有 1 次移动,没有副本。
auto ok = ap_n() + ap_n(); // 1 extra move possibly elided pre-C++17,0 extra moves since C++17
auto&& ok2 = ap_n() + ap_n(); // No moves,and lifetime extension...
因此,如果出现额外的过载,您可能会用复制来移动。
,从Jarod42's answer做笔记,以下是修改后的代码。
ap_n operator+(const ap_n& n,const ap_n& m) {
ap_n tmp {n};
tmp += n;
return tmp; // Allows copy,NRVO,or move
}
ap_n operator+(ap_n&& n,const ap_n& m) {
n += m;
return std::move(n); // Allows copy,or move
}
ap_n operator+(const ap_n& n,ap_n&& m) {
m += n;
return std::move(m); // Allows copy,or move
}
函数的数量也减少到 3 个,因为同时使用两个右值引用的函数将自动使用第二个函数。
如果我仍然误解这一点,请告诉我。