[英]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
我們可以通過附加定義做得更好,
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,我們有
如果我的理解是正確的,我們將需要每個運算符四個函數簽名。 這在某種程度上似乎不正確,因為每個操作員都有很多樣板和代碼重復。 我錯過了什么嗎? 有沒有一種優雅的方式來實現同樣的目標?
最好不要嘗試使用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);
在這種情況下,根據您的上述啟發式,只有a
或b
可能會被盜,這很奇怪。 這將導致底層資源的生命周期沒有擴展到c
,這可能違背開發人員的直覺(例如,考慮a
或b
的資源是否具有可觀察到的副作用,如日志記錄或其他系統交互)
注意:值得注意的是,C++ 中的std::string
也存在同樣的問題,其中operator+
效率低下。 重用緩沖區的一般建議是在這種情況下使用operator+=
1解決此類問題的更好方法是以某種方式創建適當的構建方法,並始終如一地使用它。 這可以通過命名良好的函數、某種適當的builder
類,或者只是使用像operator+=
這樣的復合運算operator+=
這甚至可以通過將一系列參數折疊成+=
串聯系列的模板輔助函數來完成。 假設這是在c++17或更高版本中,這可以輕松完成:
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.