【问题标题】:How to do Operator overloading with move semantics in c++? (Elegantly)如何在 C++ 中使用移动语义进行运算符重载? (优雅地)
【发布时间】:2020-12-28 22:57:51
【问题描述】:
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++ 的典型期望有点不一致。通常operator+ 作用于两个二进制操作数并返回一个 new 对象而不改变两个输入。在这种情况下修改输入对于大多数 C++ 开发人员来说都是不可取和意料之外的。这种低效率已经出现在标准类型中,例如std::string's operator+
  • @Eljay 你是对的const T&& 没有意义。我已经进行了编辑
  • @Human-Compiler 请注意,我只是重用右值。在这种情况下,修改输入并不是意外或不可取的,因为右值无论如何都会很快被销毁。重用右值可以节省移动(或者在最糟糕的情况下甚至是副本)。根据T 的大小和+ 在循环中执行的次数,它很快就会变得很重要。
  • 大多数矩阵库使用lazy operation(也有其优点/缺点)。

标签: c++ c++11 operator-overloading move-semantics generic-programming


【解决方案1】:

最好不要尝试用operator+(或任何二元运算符)窃取资源,并设计一个更合适的可能以某种方式重用数据的方法1。这应该是您构建 API 的惯用方式,如果不是唯一方式(如果您想完全避免该问题)。


C++ 中的二元运算符(如 operator+)具有一般的期望/约定,即它返回一个不同的对象而不改变其任何输入。定义一个operator+ 来使用除了左值之外的右值会引入一个非常规的接口,这会给大多数 C++ 开发人员带来困惑。

考虑您的 案例 4 示例:

c = do_something(a) + do_something(b);           // Case 4

哪个资源被盗,ab?如果a 还不足以支持b 所需的结果怎么办(假设这使用了调整大小的缓冲区)?没有一般情况可以使这成为一个简单的解决方案。

此外,没有办法区分 API 上的不同类型的右值,例如 Xvalues(std::move 的结果)和 PRvalues(返回值的函数的结果)。这意味着可以调用相同的 API:

c = std::move(a) + std::move(b);

在这种情况下,根据您的上述启发式,a b 中只有一个可能会被盗资源,这很奇怪。这会导致底层资源的生命周期不会延长c,这可能会违背开发人员的直觉(例如,如果ab 中的资源具有可观察到的副作用,例如日志记录或其他系统交互)

注意: 值得注意的是,C++ 中的std::string 也有同样的问题,其中operator+ 效率低下。重用缓冲区的一般建议是在这种情况下使用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;
}

【讨论】:

  • 从他对std::move(Obj)的使用中可以清楚地看出开发者的意图。很明显,开发人员不再对Obj 对象感兴趣。例如:vector&lt;int&gt; v1 = {1,2,3}; vector&lt;int&gt; v2; v2 = std::move(v1); v1 处于默认状态,等待销毁。移动后不再使用v1 是开发人员的责任 您的示例中的情况也是如此 c = std::move(a) + std::move(b); 表明开发人员不再对a 和b 都感兴趣。因此,他们的堆可以节省地重用
  • 这与意图无关,而是与典型期望的不对称以及出乎意料的不同生命周期有关。对象ab 可能 使底层资源的寿命更长,具体取决于库的黑盒启发式。这不是特别连贯。仅仅因为您可以在 C++ 中做某事并不意味着您应该或其他开发人员清楚该方法
  • 为此提供适当的接口,例如我所建议的,通过现有的约定使这一点变得清晰。没有歧义、混乱或任何东西的来源。是的,如果您想重载所有 4 个来执行此操作,您可以 - 但我不怀疑 C++ 开发人员会很好地响应此类代码。
  • 您的“生命周期”问题已经发生在 swap 的移动构造函数/赋值中,而不是重置输入。如果用户真的关心其移动对象的状态,他/她必须在之后手动重置(或者如果指定了状态,请参阅文档)。
【解决方案2】:

技术上是可行的。但也许你应该考虑改变设计。 代码只是一个 POC。 它有一个 UB,但它适用于 gcc 和 clang...

#include <type_traits>
#include <iostream>

    struct T {
        T()
         : pData (new size_t(1))
         , owner(true)
        { 
            
            std::cout << "malloc" << std::endl; 
        }
        ~T()
        {
            if (owner)
            {
                delete pData;
            }
        }
        T(const T &) = default;
        size_t *pData;          // Memory allocated in the constructor              
        bool   owner;           // pData ownership
        
        template <class T1, class T2>
        friend T operator+(T1 && a, T2 && b){
            
            T c(std::forward<T1>(a), std::forward<T2>(b));
            *c.pData = *a.pData + *b.pData; //UB but works
            return c;
        }
        
        private:
        template <class T1, class T2>
        T(T1 && a, T2 && b) : owner(true)
        {  
            static_assert(std::is_same_v<T, std::decay_t<T1>> && std::is_same_v<T, std::decay_t<T2>>, "only type T is supported");
            if (!std::is_reference<T1>::value)
            {
                pData = a.pData;
                a.owner = false;
                std::cout << "steal data a" << std::endl;   
            }
            else if (!std::is_reference<T2>::value)
            {
                pData = b.pData;
                b.owner = false;
                std::cout << "steal data b" << std::endl;   
            }
            else
            {
                std::cout << "malloc anyway" << std::endl;
                pData = new size_t(0);
            }            
        }
    };

int main()
{
    T a, b;
    T r = a +b; // malloc
    std::cout << *r.pData << std::endl;
    T r2 = std::move(a) + b; // no malloc
    std::cout << *r2.pData << " a: " << *a.pData << std::endl;
    T r3 = a + std::move(b); // no malloc
    std::cout << *r3.pData << " a: " << *a.pData << " b: " << *b.pData << std::endl;
    return 0;
}

【讨论】:

  • 一个好主意。请注意,bool owned 不是必需的。只需设置a.pData = nullptrdelete 内部有一个 nullptr 检查。
  • 你也可以通过将 type_traits 逻辑从构造函数移动到oprerator+来摆脱UB
  • @SreevatsankKadaveru 1. 如果窃取将指针设置为 nullptr - 那么您将无法获得所需的语法。 2 当然可以在 operator+ 中实现所有逻辑,但我的假设是您可能需要 operator '-' 等,所以我想简化 operator+
  • 查看我上面的答案,我认为它解决了问题并且没有 UB。
【解决方案3】:

这既高效又优雅,但使用了宏。


#include <type_traits>
#include <iostream>

#define OPERATOR_Fn(Op)         \
template<typename T1, typename T2>          \
friend auto operator Op (T1&& a, T2&& b)          \
           -> typename std::enable_if<std::is_same<std::decay_t<T1>,std::decay_t<T2>>::value,std::decay_t<T1>>::type \
{                                                           \
    constexpr bool a_or_b = !std::is_reference<T1>::value;            \
    std::decay_t<T1> c((a_or_b? std::forward<T1>(a) : std::forward<T2>(b)));  \
            \
   *c.pData = *c.pData Op (!a_or_b? *a.pData : *b.pData);           \
    return c;                           \
}                   \

struct T {
    T(): pData(new size_t(1)) {std::cout << "malloc" << '\n';}
    ~T() {delete pData;}
    T(const T& b): pData(new size_t(1)) { *pData = *b.pData; std::cout << "malloc" << '\n';}
    T(T&& b){
        pData = b.pData;
        b.pData = nullptr;
        std::cout<< "move constructing" << '\n';
    }

    size_t *pData;          // Memory allocated in the constructor              

    OPERATOR_Fn(+);
    OPERATOR_Fn(-);
    OPERATOR_Fn(&);
    OPERATOR_Fn(|);
};

您可以通过定义类似这样的内容来简化 type_traits 表达式以使代码更具可读性

template <typename T1, typename T2>
struct enable_if_same_on_decay{
    static constexpr bool value = std::is_same<std::decay_t<T1>, std::decay_t<T2>>::value;
    typedef std::enable_if<value,std::decay_t<T>>::type type;
};

template <typename T1, typename T2>
using enable_if_same_on_decay_t = typename enable_if_same_on_decay<T1,T2>::type;

复杂的 type_traits 表达式

-> typename std::enable_if<std::is_same<std::decay_t<T1>,std::decay_t<T2>>::value,std::decay_t<T1>>::type

变成了

-> enable_if_same_on_decay_t<T1,T2>

【讨论】:

  • 这是friending 所有可能的运算符,其中左右操作数相同(注意:这仅限于@ 987654326@)。此外,宏与它定义它的实际类型高度耦合(因为它硬编码pData),所以这不是一个特别可扩展的解决方案。
猜你喜欢
  • 2013-04-14
  • 1970-01-01
  • 2011-12-11
  • 2014-01-02
  • 1970-01-01
  • 1970-01-01
  • 2019-09-14
  • 2019-04-12
  • 1970-01-01
相关资源
最近更新 更多