簡體   English   中英

C++11 右值和移動語義混淆(返回語句)

[英]C++11 rvalues and move semantics confusion (return statement)

我正在嘗試理解 C++11 的右值引用和移動語義。

這些示例之間有什么區別,其中哪些將不進行矢量復制?

第一個例子

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

第二個例子

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

第三個例子

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

第一個例子

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

第一個示例返回一個由rval_ref捕獲的臨時rval_ref 該臨時文件的生命周期將超過rval_ref定義,您可以像按值捕獲它一樣使用它。 這與以下內容非常相似:

const std::vector<int>& rval_ref = return_vector();

除了在我的重寫中,您顯然不能以非常量的方式使用rval_ref

第二個例子

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

在第二個示例中,您創建了一個運行時錯誤。 rval_ref現在持有對函數內部被破壞的tmp的引用。 運氣好的話,這段代碼會立即崩潰。

第三個例子

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

您的第三個示例大致相當於您的第一個示例。 tmp上的std::move是不必要的,實際上可能是一種性能悲觀,因為它會抑制返回值優化。

對您正在做的事情進行編碼的最佳方法是:

最佳實踐

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

即就像在 C++03 中一樣。 tmp在 return 語句中被隱式視為右值。 它將通過返回值優化(無復制,無移動)返回,或者如果編譯器決定它不能執行 RVO,那么它將使用 vector 的移動構造函數來執行 return 僅當未執行 RVO 並且返回的類型沒有移動構造函數時,才會使用復制構造函數進行返回。

它們都不會復制,但第二個將引用被破壞的向量。 命名的右值引用幾乎從不存在於常規代碼中。 您可以按照在 C++03 中編寫副本的方式編寫它。

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

除了現在,向量被移動。 在絕大多數情況下,類的用戶不會處理它的右值引用。

簡單的答案是,您應該像編寫常規引用代碼一樣為右值引用編寫代碼,並且應該在 99% 的時間里在精神上對待它們。 這包括所有關於返回引用的舊規則(即永遠不要返回對局部變量的引用)。

除非您正在編寫需要利用 std::forward 並能夠編寫接受左值或右值引用的通用函數的模板容器類,否則這或多或少是正確的。

移動構造函數和移動賦值的一大優點是,如果您定義它們,編譯器可以在無法調用 RVO(返回值優化)和 NRVO(命名返回值優化)的情況下使用它們。 這對於從方法有效地按值返回昂貴的對象(如容器和字符串)來說非常重要。

現在,右值引用的有趣之處在於,您還可以將它們用作普通函數的參數。 這允許您編寫具有常量引用(const foo& other)和右值引用(foo&& other)重載的容器。 即使參數太笨重而無法僅通過構造函數調用傳遞,它仍然可以完成:

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

STL 容器已更新為幾乎所有內容(哈希鍵和值、向量插入等)都具有移動重載,並且是您最常看到它們的地方。

您也可以將它們用於普通函數,如果您只提供一個右值引用參數,您可以強制調用者創建對象並讓函數執行移動。 這與其說是一個很好的用途,不如說是一個示例,但在我的渲染庫中,我為所有加載的資源分配了一個字符串,以便更容易查看每個對象在調試器中代表什么。 界面是這樣的:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

這是一種“泄漏抽象”的形式,但允許我利用我在大多數情況下必須創建字符串的事實,並避免再次復制它。 這不是完全高性能的代碼,但是當人們掌握了這個功能時,它是一個很好的例子。 這段代碼實際上要求變量要么是調用的臨時變量,要么是調用 std::move:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

或者

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

或者

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

但這不會編譯!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

本身不是答案,而是指南。 大多數情況下,聲明本地T&&變量沒有多大意義(就像你對std::vector<int>&& rval_ref )。 您仍然需要std::move()它們才能在foo(T&&)類型方法中使用。 還有一個已經提到的問題,當您嘗試從函數返回這樣的rval_ref ,您將獲得標准的銷毀臨時慘敗參考。

大多數時候我會采用以下模式:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

您沒有對返回的臨時對象持有任何引用,因此您可以避免希望使用移動對象的(沒有經驗的)程序員的錯誤。

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

顯然,在某些情況下(盡管相當少見),函數真正返回T&&是對可以移動到對象中的非臨時對象的引用。

關於 RVO:這些機制通常有效,編譯器可以很好地避免復制,但在返回路徑不明顯的情況下(例外, if條件決定了你將返回的命名對象,並且可能與其他對象結合)rrefs 是你的救星(即使可能更貴)。

這些都不會做任何額外的復制。 即使不使用 RVO,新標准也表明,在我認為返回時,移動構造優先於復制。

我確實相信您的第二個示例會導致未定義的行為,因為您正在返回對局部變量的引用。

正如在第一個答案的評論中已經提到的, return std::move(...); 在返回局部變量以外的情況下,construct 可以有所作為。 這是一個可運行的示例,它記錄了當您使用和不使用std::move()返回成員對象時會發生什么:

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

據推測, return std::move(some_member); 僅當您確實想要移動特定的類成員時才有意義,例如在class C代表短期適配器對象的情況下,其唯一目的是創建struct A實例。

請注意struct A總是如何從class B復制出來,即使class B class B對象是 R 值也是如此。 這是因為編譯器無法告訴class Bstruct A實例將不再使用。 class C ,編譯器確實從std::move()獲得了此信息,這就是為什么struct A移動的原因,除非class C的實例是常量。

暫無
暫無

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

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