[英]Move Semantics and Pass-by-Rvalue-Reference in Overloaded Arithmetic
我正在使用C ++編寫一個小型數值分析庫。 我一直在嘗試使用最新的C ++ 11功能實現,包括移動語義。 我理解以下帖子中的討論和最佳答案: C ++ 11 rvalues和移動語義混淆(return語句) ,但有一種情況我仍然試圖包圍我的頭腦。
我有一個類,稱之為T
,它配備了重載運算符。 我也有復制和移動構造函數。
T (const T &) { /*initialization via copy*/; }
T (T &&) { /*initialization via move*/; }
我的客戶端代碼大量使用運算符,所以我試圖確保復雜的算術表達式從移動語義中獲得最大的好處。 考慮以下:
T a, b, c, d, e;
T f = a + b * c - d / e;
沒有移動語義,我的操作符每次都使用復制構造函數創建一個新的局部變量,所以總共有4個副本。 我希望通過移動語義,我可以將其減少到2個副本加上一些動作。 在括號中:
T f = a + (b * c) - (d / e);
(b * c)
和(d / e)
必須以通常的方式創建臨時副本,但是如果我可以利用其中一個臨時數來僅用移動來累積剩余的結果,那將是很好的。
使用g ++編譯器,我已經能夠做到這一點,但我懷疑我的技術可能不安全,我想完全理解為什么。
以下是加法運算符的示例實現:
T operator+ (T const& x) const
{
T result(*this);
// logic to perform addition here using result as the target
return std::move(result);
}
T operator+ (T&& x) const
{
// logic to perform addition here using x as the target
return std::move(x);
}
如果沒有調用std::move
,那么只調用每個運算符的const &
版本。 但是當如上所述使用std::move
時,使用每個運算符的&&
版本執行后續算法(在最內層表達式之后)。
我知道RVO可以被抑制,但在計算成本非常高的現實問題上,似乎收益略微超過了RVO的缺乏。 也就是說,當我包含std::move
時,在數百萬次計算中,我確實獲得了非常小的加速。 雖然說實話,但沒有足夠快。 我真的只想完全理解這里的語義。
是否有一位C ++ Guru願意花時間以簡單的方式解釋我是否以及為何使用std :: move在這里是一件壞事? 提前謝謝了。
您應該更喜歡將運算符重載為自由函數以獲得完全類型對稱(可以在左側和右側應用相同的轉換)。 這使得你在問題中遺漏的內容更加明顯。 將您的操作員重新設置為您提供的免費功能:
T operator+( T const &, T const & );
T operator+( T const &, T&& );
但是你沒有提供一個處理左側是臨時的版本:
T operator+( T&&, T const& );
並且當兩個參數都是rvalues時,為了避免代碼中出現歧義,您需要提供另一個重載:
T operator+( T&&, T&& );
常見的建議是將+=
實現為修改當前對象的成員方法,然后將operator+
編寫為轉換器,以修改接口中的相應對象。
我真的沒有想過這么多,但是可能有一個使用T
(沒有r / lvalue引用)的替代方案,但是我擔心它不會減少你需要提供的重載次數,以便在所有情況下使operator+
高效。
以其他人所說的為基礎:
T::operator+( T const & )
調用std::move
是不必要的,可以防止RVO。 operator+
委托給T::operator+=( T const & )
。 我還想補充一點,完美轉發可用於減少所需的非成員operator+
重載次數:
template< typename L, typename R >
typename std::enable_if<
std::is_convertible< L, T >::value &&
std::is_convertible< R, T >::value,
T >::type operator+( L && l, R && r )
{
T result( std::forward< L >( l ) );
result += r;
return result;
}
對於一些運算符來說,這個“通用”版本就足夠了,但由於加法通常是可交換的,我們可能想檢測右手操作數何時是右值並修改它而不是移動/復制左手操作數。 這需要一個版本作為左值的右側操作數:
template< typename L, typename R >
typename std::enable_if<
std::is_convertible< L, T >::value &&
std::is_convertible< R, T >::value &&
std::is_lvalue_reference< R&& >::value,
T >::type operator+( L && l, R && r )
{
T result( std::forward< L >( l ) );
result += r;
return result;
}
另一個是右手操作數,它們是右值:
template< typename L, typename R >
typename std::enable_if<
std::is_convertible< L, T >::value &&
std::is_convertible< R, T >::value &&
std::is_rvalue_reference< R&& >::value,
T >::type operator+( L && l, R && r )
{
T result( std::move( r ) );
result += l;
return result;
}
最后,您可能也對Boris Kolpackov和Sumant Tambe提出的技術以及Scott Meyers對這個想法的回應感興趣。
我同意DavidRodríguez認為使用非成員operator+
函數是一個更好的設計,但我會把它放在一邊,專注於你的問題。
寫作時,你會發現性能下降,我感到很驚訝
T operator+(const T&)
{
T result(*this);
return result;
}
代替
T operator+(const T&)
{
T result(*this);
return std::move(result);
}
因為在前一種情況下,編譯器應該能夠使用RVO在內存中為函數的返回值構造result
。 在后一種情況下,編譯器需要將result
移動到函數的返回值中,因此會產生額外的移動成本。
一般來說,假設你有一個函數返回一個對象(即,不是引用),這種事情的規則是:
std::move
應用於它。 這允許編譯器執行RVO,這比副本或移動便宜。 std::move
應用於它。 這會將參數轉換為右值,從而允許編譯器從中移動。 如果只返回參數,編譯器必須執行返回值的副本。 &&
”參數,可以是右值引用或左值引用),請將std::forward
應用於它。 沒有它,編譯器必須執行返回值的副本。 有了它,如果引用綁定到右值,編譯器可以執行移動。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.