首页 > 解决方案 > 如何在 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

我们可以通过额外定义来做得更好,

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,我们有

如果我所有的理解都是正确的,我们将需要每个运算符四个函数签名。这在某种程度上似乎并不正确,因为每个操作员都有很多样板和代码重复。我错过了什么吗?有没有一种优雅的方式来实现同样的目标?

标签: c++c++11operator-overloadingmove-semanticsgeneric-programming

解决方案


最好不要尝试使用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,这可能违背开发人员的直觉(例如,考虑资源是否存在ab具有可观察到的副作用,如日志记录或其他系统交互)

注意:值得注意的是,std::string在 C++ 中也有同样的问题, whereoperator+效率低下。重用缓冲区的一般建议是operator+=在这种情况下使用


1对此类问题的更好解决方案是以某种方式创建适当的构建方法,并始终如一地使用它。这可能是通过命名良好的函数、某种适当builder的类,或者只是使用复合运算符,如operator+=

这甚至可以通过将一系列参数折叠成+=串联系列的模板辅助函数来完成。假设这是在或更高版本中,这可以很容易地完成:

template <typename...Args>
auto concat(Args&&...args) -> SomeType
{
    auto result = SomeType{}; // assuming default-constructible

    (result += ... += std::forward<Args>(args));
    return result;
}

推荐阅读