簡體   English   中英

如何在 C++ 中使用移動語義進行運算符重載? (優雅)

[英]How to do Operator overloading with move semantics in c++? (Elegantly)

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+來操作 Rvalues 和 Lvalues 會引入一個非常規的接口,這會引起大多數 C++ 開發人員的困惑。

考慮您的案例 4示例:

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

哪個資源被盜了, a還是b 如果a的大小不足以支持b所需的結果(假設這使用了調整大小的緩沖區)怎么辦? 沒有一般情況使這成為一個簡單的解決方案。

此外,無法區分像 Xvalues( std::move的結果)和 PRvalues(返回值的函數的結果)這樣的 API 上的不同類型的 Rvalues。 這意味着您可以調用相同的 API:

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

在這種情況下,根據您的上述啟發式,只有ab可能會被盜,這很奇怪。 這將導致底層資源的生命周期沒有擴展c ,這可能違背開發人員的直覺(例如,考慮ab的資源是否具有可觀察到的副作用,如日志記錄或其他系統交互)

注意:值得注意的是,C++ 中的std::string也存在同樣的問題,其中operator+效率低下。 重用緩沖區的一般建議是在這種情況下使用operator+=


1解決此類問題的更好方法是以某種方式創建適當的構建方法,並始終如一地使用它。 這可以通過命名良好的函數、某種適當的builder類,或者只是使用像operator+=這樣的復合運算operator+=

這甚至可以通過將一系列參數折疊成+=串聯系列的模板輔助函數來完成。 假設這是在或更高版本中,這可以輕松完成:

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

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

技術上是可行的。 但也許您應該考慮更改設計。 代碼只是一個 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;
}

這是高性能且優雅的,但使用了宏。


#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>

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM