簡體   English   中英

復制ctor調用而不是移動ctor

[英]Copy ctor called instead of move ctor

為什么從bar而不是move構造函數返回時調用了復制構造函數?

#include <iostream>

using namespace std;

class Alpha {
public:
  Alpha() { cout << "ctor" << endl; }
  Alpha(Alpha &) { cout << "copy ctor" << endl; }
  Alpha(Alpha &&) { cout << "move ctor" << endl; }
  Alpha &operator=(Alpha &) { cout << "copy asgn op" << endl; }
  Alpha &operator=(Alpha &&) { cout << "move asgn op" << endl; }
};

Alpha foo(Alpha a) {
  return a; // Move ctor is called (expected).
}

Alpha bar(Alpha &&a) {
  return a; // Copy ctor is called (unexpected).
}

int main() {
  Alpha a, b;
  a = foo(a);
  a = foo(Alpha());
  a = bar(Alpha());
  b = a;
  return 0;
}

如果bar確實return move(a)則行為符合預期。 我不明白為什么調用std::move是必要的,因為foo在返回時會調用move構造函數。

在這種情況下有兩件事需要理解:

  1. a in bar(Alpha &&a)是一個命名的右值參考; 因此,被視為左值。
  2. a仍然是一個參考。

第1部分

由於abar(Alpha &&a)是一個命名右值引用,其作為左值處理。 將命名的右值引用視為左值的動機是提供安全性。 考慮以下,

Alpha bar(Alpha &&a) {
  baz(a);
  qux(a);
  return a;
}

如果baz(a)a視為rvalue,則可以自由調用move構造函數,而qux(a)可能無效。 該標准通過將命名的右值引用視為左值來避免此問題。

第2部分

由於a仍然是引用(並且可以引用bar范圍之外的對象), bar在返回時調用復制構造函數。 這種行為背后的動機是提供安全。

參考

  1. 所以Q&A - 通過右值參考返回
  2. Kerrek SB的評論

是的,非常困惑。 我想舉另一個SO張貼在這里implicite移動 在哪里我發現以下評論有點令人信服,

因此,標准委員會決定您必須明確任何命名變量的移動,無論其引用類型如何

實際上“&&”已經表示放手了,當你做“返回”時,它就足夠安全了。

可能它只是標准委員會的選擇。

scott meyers的“有效的現代c ++”第25項,也總結了這一點,沒有給出太多解釋。

Alpha foo() {
  Alpha a
  return a; // RVO by decent compiler
}
Alpha foo(Alpha a) {
  return a; // implicit std::move by compiler
}

Alpha bar(Alpha &&a) {
  return a; // Copy ctor due to lvalue
}

Alpha bar(Alpha &&a) {
  return std:move(a); // has to be explicit by developer
}

當人們首先了解右值參考時,這是一個非常常見的錯誤。 基本問題是類型價值類別之間的混淆。

int是一種類型。 int&是一種不同的類型。 int&&是另一種類型。 這些都是不同的類型。

左值和右值是稱為值類別的東西。 請查看這里的精彩圖表: rvalues,lvalues,xvalues,glvalues和prvalues是什么? 你可以看到除了左值和右值之外,我們還有prvalues和glvalues和xvalues,它們形成了各種維恩圖的關系。

C ++有一些規則可以說各種類型的變量可以綁定到表達式。 然而,表達式引用類型被丟棄(人們經常說表達式沒有引用類型)。 相反,表達式有一個值類別,它確定哪些變量可以綁定到它。

換句話說:右值引用和左值引用僅與賦值的左側直接相關,即創建/綁定變量。 在右側,我們討論的是表達式而不是變量,而rvalue / lvalue reference-ness僅與確定值類別的上下文相關。

一個非常簡單的例子就是簡單地看一下純粹的int類型的東西。 int作為表達式的變量是左值。 但是,由計算返回int的函數組成的表達式是rvalue。 這對大多數人來說都是直觀的; 關鍵是要分離表達式的類型(甚至在引用被丟棄之前)和它的值類別。

這導致的是,即使int&&類型的變量只能綁定到rvalues,也不意味着所有類型為int&&表達式都是 rvalues。 實際上,正如http://en.cppreference.com/w/cpp/language/value_category中的規則所述,任何由命名變量組成的表達式總是一個左值, 無論類型如何

這就是為什么你需要std::move以便將rvalue引用傳遞給rvalue引用的后續函數。 這是因為右值引用不會綁定到其他右值引用。 它們綁定到右值。 如果要獲取移動構造函數,則需要為其綁定一個rvalue,並且命名的rvalue引用不是rvalue。

std::move是一個返回右值引用的函數。 這種表達的價值范疇是什么? 一個右值? 不。 這是一個xvalue。 這基本上是一個右值,有一些額外的屬性。

foobar ,表達式a是左值。 聲明return a; 表示從初始化程序a初始化返回值對象,並返回該對象。

這兩種情況之間的區別在於,對於這個初始化重載解析取決於是否進行不同地a聲明為最內層的閉合塊內的非易失性自動對象,或一個函數參數。

它適用於foo而不是bar (在bara被聲明為參考)。 所以return a; foo選擇move構造函數來初始化返回值,但return a; bar選擇復制構造函數。

全文是C ++ 14 [class.copy] / 32:

當滿足復制/移動操作的省略標准時,但不滿足異常聲明,並且要復制的對象由左值指定,或者當返回語句中的表達式是(可能帶有括號的)id-時表達式,用於在最內層封閉函數或lambda-expression的body或parameter-declaration-clause中聲明的具有自動存儲持續時間的對象,首先執行重載決策以選擇復制的構造函數,就像對象由rvalue指定一樣。 如果第一個重載決策失敗或未執行,或者所選構造函數的第一個參數的類型不是對象類型的rvalue引用(可能是cv-qualified),則再次執行重載決策,將對象視為左值。 [注意:無論是否發生復制省略,都必須執行此兩階段重載決策。 如果未執行elision,它將確定要調用的構造函數,並且即使調用被省略,也必須可以訪問所選的構造函數。 - 尾注]

其中“符合復制/移動操作的標准”是指[class.copy] /31.1:

  • 在具有類返回類型的函數的return語句中,當表達式是具有與函數返回類型相同的cv-unqualified類型的非易失性自動對象(函數或catch子句參數除外)的名稱時,通過將自動對象直接構造到函數的返回值中,可以省略復制/移動操作

注意,這些文本將改變為C ++ 17。

暫無
暫無

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

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