c++ - 如何在 C++ 中使用移动语义进行运算符重载?(优雅地)
问题描述
class T {
size_t *pData; // Memory allocated in the constructor
friend T operator+(const T& a, const T& b);
};
T operator+(const T& a, const T& b){ // Op 1
T c; // malloc()
*c.pData = *a.pData + *b.pData;
return c;
}
T do_something(){
/* Implementation details */
return T_Obj;
}
一个简单class T
的动态内存。考虑
T a,b,c;
c = a + b; // Case 1
c = a + do_something(b); // Case 2
c = do_something(a) + b; // Case 3
c = do_something(a) + do_something(b); // Case 4
- 案例 1 使用 1 个 malloc()
- 案例 2 使用 2 malloc()
- 案例 3 使用 2 malloc()
- 案例 4 使用 3 malloc()
我们可以通过额外定义来做得更好,
T& operator+(const T& a, T&& b){ // Op 2
// no malloc() steeling data from b rvalue
*b.pData = *a.pData + *b.pData;
return b;
}
案例 2 现在只使用了 1 个 malloc(),但是案例 3 呢?我们需要定义 Op 3 吗?
T& operator+(T&& a, const T& b){ // Op 3
// no malloc() steeling data from a rvalue
*b.pData = *a.pData + *b.pData;
return b;
}
此外,如果我们确实定义了 Op 2 和 Op 3,鉴于右值引用可以绑定到左值引用这一事实,编译器现在有两个同样合理的函数定义可以在案例 4 中调用
T& operator+(const T& a, T&& b); // Op 2 rvalue binding to a
T& operator+(T&& a, const T& b); // Op 3 rvalue binding to b
编译器会抱怨一个模棱两可的函数调用,定义 Op 4 是否有助于解决编译器的模棱两可的函数调用问题?因为我们没有通过 Op 4 获得额外的性能
T& operator+(T&& a, T&& b){ // Op 4
// no malloc() can steel data from a or b rvalue
*b.pData = *a.pData + *b.pData;
return b;
}
对于 Op 1、Op 2、Op 3 和 Op 4,我们有
- 案例1:1个malloc(调用Op 1)
- 案例2:1个malloc(调用Op 2)
- 案例3:1个malloc(调用Op 3)
- 案例4:1个malloc(调用Op 4)
如果我所有的理解都是正确的,我们将需要每个运算符四个函数签名。这在某种程度上似乎并不正确,因为每个操作员都有很多样板和代码重复。我错过了什么吗?有没有一种优雅的方式来实现同样的目标?
解决方案
最好不要尝试使用operator+
(或任何二元运算符)窃取资源,并设计一个更合适的可以以某种方式重用数据的方法1。这应该是您构建 API 的惯用方式,如果不是唯一方式(如果您想完全避免该问题)。
C++ 中operator+
的二元运算符有一个普遍的期望/约定,即它返回一个不同的对象而不改变它的任何输入。除了左值之外,定义一个operator+
与右值一起操作的方法会引入一个非常规的接口,这会给大多数 C++ 开发人员带来困惑。
考虑您的案例 4示例:
c = do_something(a) + do_something(b); // Case 4
哪个资源被盗,a
或者b
?如果a
还不足以支持所需的结果怎么办b
(假设这使用了调整大小的缓冲区)?没有一般情况可以使它成为一个简单的解决方案。
此外,无法区分 API 上的不同类型的 Rvalue,例如 Xvalues(的结果std::move
)和 PRvalues(返回值的函数的结果)。这意味着您可以调用相同的 API:
c = std::move(a) + std::move(b);
在这种情况下,根据您的上述启发式,只有一个a
或 b
可能会被盗其资源,这很奇怪。这将导致底层资源的生命周期不会被延长到c
,这可能违背开发人员的直觉(例如,考虑资源是否存在a
或b
具有可观察到的副作用,如日志记录或其他系统交互)
注意:值得注意的是,std::string
在 C++ 中也有同样的问题, whereoperator+
效率低下。重用缓冲区的一般建议是operator+=
在这种情况下使用
1对此类问题的更好解决方案是以某种方式创建适当的构建方法,并始终如一地使用它。这可能是通过命名良好的函数、某种适当builder
的类,或者只是使用复合运算符,如operator+=
这甚至可以通过将一系列参数折叠成+=
串联系列的模板辅助函数来完成。假设这是在c++17或更高版本中,这可以很容易地完成:
template <typename...Args>
auto concat(Args&&...args) -> SomeType
{
auto result = SomeType{}; // assuming default-constructible
(result += ... += std::forward<Args>(args));
return result;
}
推荐阅读
- node.js - 在 Node eml 文件中添加“CC”收件人
- sql-server - 使用 Golden 记录创建交叉引用表并将其他记录关联到该记录
- python - 递归函数中的计数器
- file - 如何知道文件是否是二进制文件
- akka - 这两种集群配置有什么区别?
- ruby-on-rails - 如何将 image_tag 与 srcset 和活动存储变体一起使用?
- python-3.x - 如何修复 Nginx 与套接字的连接失败(权限被拒绝)
- r - 您可以使用 .RData 对象保存 R 代码文件以进行版本控制吗?
- powerbi - power bi 直方图,频率是不同的日期,值是每个日期的值的总和
- html - CSS网格列周围的框阴影