2016-08-04 62 views
2

我想更好地瞭解如何在C++中移動工作。編譯器如何知道何時移動以及何時不移動?編譯器何時在C++中移動/複製?

#include <iostream> 

template <typename T> 
struct Container { 
    T value; 
    Container (T value): value(value) {} 
    Container (const Container & i): value(i.value) { 
     std::cout << "copying" << std::endl; 
    } 
    Container (Container && i): value(std::move(i.value)) { 
     std::cout << "moving" << std::endl; 
    } 
}; 

void increment (Container<int> n) { 
    std::cout << "incremented to " << ++n.value << std::endl; 
} 

int main() { 
    Container<int> n (5); 
    increment(std::move(n)); 
    std::cout << n.value << std::endl; 
} 

這個例子打印

moving 
incremented to 6 
5 

所以我預計int已被轉移,但當時我不應該能夠仍然使用它之後(並獲得原始值)。

好吧,所以也許int被複制,因爲value(std::move(i.value))在移動構造函數中複製它。但我仍然不明白爲什麼Container<int> n仍然在它被移動後仍然存在。

+1

請注意,通過優化,由於副本構造函數(以及我相信也是移動構造函數)中的副作用可以忽略,所以可能永遠不會顯示您的輸出語句。 – rubenvb

+0

「所以我期望'int'已經被移動了,但之後我不能繼續使用它」爲什麼不呢?編譯器是否應該用垃圾數據和每一個獲得'std :: move'd的原始類型來排除錯誤,只是爲了與錯誤的移動語義定義相對應?這將代表反向優化。遷移的存在是爲了實現資源的最佳轉移。根據定義,原始類型沒有任何資源。 –

+0

@rubenvb這是真的,但不是在_all_情況下。例如,對於初始化和函數返回,允許複製elision。但是,我不相信在OP的例子中它是被允許的。 – davmac

回答

1

所以我期望int已經被移動了,但是之後我不能夠繼續使用它(並且獲得原始值)。

C++中的「移動」實際上並未移動內存。對於像字符串和向量這樣的複雜類型,「移動」意味着內部指針交換而不是複製所有指向的數據。但是對於int,沒有捷徑可以完成,因此只需複製數據。這就是爲什麼你的原始價值仍然存在,你仍然可以閱讀它。

C++編譯器如何知道何時移動以及何時不移動?

它不!

可移動類的作者呢。通常情況下,你將這個「指針交換」邏輯放入一個「移動構造函數」中,這是構造函數採用右值引用的一個奇特名稱。自C++ 11以來的重載解析規則的設計使得無論何時你傳入一個臨時文件(或者通過作弊得到一個右值,通過編寫std::move(這個命名是因爲它不會移動任何東西!)),因爲這些是你通常想要「移出」的對象。

但是,如果您不在移動構造函數中編寫任何「移動」代碼,那麼編譯器不會爲您創造一些「移動」代碼。

但我仍不明白爲什麼Container<int> n仍然在它被肯定被移動後。

您沒有編寫任何代碼,在將它傳遞給移動構造函數後,會將原始Container<int>保留爲任何其他狀態。

此外,移動構造函數不會自動執行任何類似操作。這只是一個地方你的移動邏輯!

這裏是Container爲其替代版本移動可觀察:

template <typename T> 
struct Container 
{ 
    T* ptr; 
    Container(T value) : ptr(new T(value)) {} 
    Container(const Container& i): ptr(new T(*i.ptr)) { 
     std::cout << "copying" << std::endl; 
    } 
    Container(Container&& i) { 
     ptr = i.ptr; 
     i.ptr = nullptr; 
     // no data copied - we just "steal" the data by swapping pointers! 
     std::cout << "moving" << std::endl; 
    } 
    ~Container() { delete ptr; } 
}; 

(我用原始指針是明確的,因爲我可以對正在發生的事情,在現實中,你會使用std::unique_ptr實際上做同樣的事情,你在自己移動構造函數!)

5

std::move確實只是將一個值更改爲可移動的右值。如果將此應用於int,則它沒有實際效果,因爲「移動」int值沒有任何意義。 (的確,這個值仍然是一個右值;只是有一個參考int的右值通常沒有比其他任何類型的參考對int有用)。

這是因爲移動意味着大約從一個物體轉移資源到另一個,在複製這些資源(通過複製他們)需要避免這樣的方式 - 因爲這樣的重複可以不平凡;首先,它可能需要動態內存分配。複製一個int值是微不足道的,所以不需要特殊的移動語義。

因此,應用於您的示例,移動Container<int>與複製它完全相同,除了輸出(「移動」與「複製」)之外。 (請注意,即使是移動也需要源對象在操作完成後保持有效狀態 - 它不會銷燬源對象,因爲您似乎認爲它可能應該)。

至於編譯器如何知道它何時可以移動與複製,這是類型類別的問題。您使用std::move明確地將值的類型類別更改爲右值(或更具體地說,更改爲xvalue),並且此類型的值可以與移動構造函數中的右值引用參數相匹配。一般而言,使用右值參考參數的過載優於非右值參考參數(精確的規則複雜)。

產生一個右值的另一常用方法是因爲未命名的臨時 - 通過其中結果未綁定到一個變量將對象或值(a + b,其中ab都是int類型上執行一些操作,是一個簡單的例子 - 結果是一個臨時對象;它不存在於它自己的變量中)。當一個更復雜的對象是一個臨時對象時,將它移動到其最終目標可能比複製它更有效,並且是安全的,因爲移動後的對象的不確定狀態以後不能使用。所以,這些值也是rvalues,並且會綁定到右值引用(並且可能會被移動)。

+0

措辭狡辯。它不會將引用更改爲右值引用。它將一個左值變成一個xvalue,只是保留了prvalue-ness。 – Barry

+0

@Barry'std :: move'相當於對一個右值引用類型使用'static_cast',所以從這個意義上說,改變(或者說,我想,轉換)一個對右值引用的引用。在我看來,重要的事情 - 特別是關於這個例子 - 是結果是一個右值引用,並且綁定到一個右值引用參數(比如在移動構造函數中)。是否是一個xvalue並不重要。 – davmac

+1

@Barry(不過,我已經更新了答案 - 因爲你是對的,說「更改爲右值」比「右值參考」更正確)。 – davmac

1

此舉僅適用於具有非平凡狀態的對象,否則複製起來會很昂貴。在移動結束時,原始值和新值都應處於有效狀態。新的價值應該等同於舊價值,而且你通常不知道舊價值的狀態不是有效的。

作爲一個例子,對於一個向量來說,將內容從一個拷貝到另一個是很昂貴的,所以移動只會交換內容。這樣更有效率,並且有獎勵,它不會失敗。移動之後,您仍然可以使用舊的矢量,但移動之前它不會處於相同的狀態。

2

所以我預計INT已被轉移

原始類型,如int沒有移動構造函數,它們只是複製。

但當時我不應該能夠仍然使用它之後

那不是怎樣動人的藝術品。你仍然可以使用移動的對象 - 它會處於有效狀態。但是,該狀態是不確定的,因此您不能指望該對象具有任何特定的值,也不能使用具有先決條件的操作。據我所知,有int對象具有前提條件沒有任何操作。

好吧,所以也許int被複制,因爲值(std :: move(i.value))將它複製到移動構造函數中。

是的,它被複制。

但是我仍然不明白爲什麼Container n在它肯定被移動後仍然存在。

移動後對象不會消失。一般來說,它們處於有效但不確定的狀態。您編寫的這個特定的移動構造函數恰好使對象與移動之前的狀態完全相同。


當該編譯器的舉動......在C++

當副本初始化是帶一個非const右值參數對象的類型有一個移動構造函數。或者當一個對象被賦予一個非常量r值操作數,並且該對象的類型有一個移動賦值運算符。

編譯器如何知道何時移動以及何時不移動?

編譯器知道所有表達式的類型。特別是,它知道表達式的類型是否是非常量右值。編譯器也知道你可以移動的所有類型的定義。根據定義,編譯器知道某個類型是否具有移動構造函數。

0

一旦對象已經被移動,沒有什麼說你不能事後用它的標準。諸如Sean Parent之類的頂級C++開發人員已經抱怨這一點,並希望標準能夠「更好」地執行某些操作。移動對象是爲了優化,可以讓移動的對象處於任何狀態。因此,移動原語可以由編譯器實現爲直接副本,而僅保留原始值。

爲了更好地瞭解如何移動對象而不編寫一些複雜的對象,只需使用填充了幾個值的std :: vector即可。在第一個電話中,我們正在建立一個基本案例。該向量通過引用傳遞給const,以便在函數的參數上調用複製構造函數。在第二次調用中,vector是作爲右值引用傳遞的,因爲當我們調用std :: move時,我們將它轉​​換爲此值。在這種情況下,矢量的移動構造函數將被調用。在內部,新值(函數的參數)從傳入的右值引用中獲取內部指針。然後它將傳入的向量的內部指針設置爲類似於空向量的東西。我們在第三次電話會議上看到了這一點,我們再製作一份。但是,該向量在第二次調用中移動,因此第三次調用的副本中沒有元素。

#include <iostream> 
#include <vector> 

void print_size(std::vector<int> value) { 
    std::cout << value.size() << std::endl; 
} 

int main() { 
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
    std::cout << "Pass by reference to const: "; 
    print_size(v); 
    std::cout << "Pass by rvalue reference: "; 
    print_size(std::move(v)); 
    std::cout << "Pass by reference to const: "; 
    print_size(v); 
    return 0; 
}