簡體   English   中英

在重載算法中移動語義和Rvalue-Reference

[英]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 KolpackovSumant 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,這比副本或移動便宜。
  • 如果要返回類型為rvalue的參數, std::move應用於它。 這會將參數轉換為右值,從而允許編譯器從中移動。 如果只返回參數,編譯器必須執行返回值的副本。
  • 如果您返回的是一個通用引用的參數(即推導類型的“ && ”參數,可以是右值引用或左值引用),請將std::forward應用於它。 沒有它,編譯器必須執行返回值的副本。 有了它,如果引用綁定到右值,編譯器可以執行移動。

暫無
暫無

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

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