C++右值引用和完美转发
C++右值引用和完美轉發
- 何為引用
- 引用必須是左值
- 右值引用
- 完美轉發
- move()
- 使用move的優點
- move 左值測試
- move 右值測試
- 注意
- 參考鏈接
看到有些同學,調用函數的時候總喜歡使用std::move希望避免一些開銷,而實際上由于他并不理解什么是右值引用、完美轉發,導致這種努力成為了徒勞,反增笑柄。
本文為記錄我學習右值引用和完美轉發的筆記。
何為引用
C++新增了一種復合類型,也就是引用變量。通過引用,就可以使用該引用名稱或變量名稱來指向變量。
引用必須是左值
對于對象的引用必須是左值(常量引用除外)
const引用能夠綁定到臨時對象, 并將臨時對象的生命周期由”創建臨時對象的完整表達式”提升至”綁定到的const引用超出作用域”。 non-const 引用沒有這個功能
右值引用
右值引用可以從字面意思上理解,指的是以引用傳遞(而非值傳遞)的方式使用 C++ 右值。用 “&&” 表示。
完美轉發
- 使用forward()再模板可以做到完美轉發,減少拷貝
- 完美轉發的好處是函數可以動態的接受函數參數,從而免去了有可能的拷貝
- 完美轉發的函數內部,需要使用forward()來把接受的參數轉化為合適的形式傳遞出去
我們看Chromium提供的例子:
#include <cstdio> #include <utility>class MyType {public:MyType() {}MyType(MyType&& other) { fprintf(stderr, "move ctor\n"); }MyType(const MyType& other) { fprintf(stderr, "copy ctor\n"); }; };void Store(const MyType& type) {fprintf(stderr, "store (copy)\n"); }void Store(MyType&& type) {fprintf(stderr, "store (move)\n"); }template<typename T> void ProcessAndStore(T&& var) {// Process// ...// The type of |var| could be an rvalue reference, which means we should pass// an rvalue to Store. However, it could also be an lvalue reference, which// means we should pass an lvalue.// Note that just doing Store(var); will always pass an lvalue and doing// Store(std::move(var)) will always pass an rvalue. Forward does the right// thing by casting to rvalue only if var is an rvalue reference.Store(std::forward<T>(var)); }int main(int argc, char **argv) {MyType type;// In ProcessAndStore: T = MyType&, var = MyType&ProcessAndStore(type);// In ProcessAndStore: T = MyType, var = MyType&&ProcessAndStore(MyType()); }move()
-
這個函數從C++11開始也變為STL函數了
-
移動賦值函數
-
std::move函數可以以非常簡單的方式將左值引用轉換為右值引用
-
應用之一是unique_ptr
-
移動之后括號內的值就不要再用了, move之后本身就會析構, functions that receive rvalues may act destructively on your variable, so using the variable’s contents afterward may result in undefined behaviour. The only valid things you may do after calling std::move() on a variable are:
- Destroy it
- Assign to it (ie. replace its contents)
- 上一點的原因是,rvalue reference的語義是函數可以認為rvalue外面沒有再被引用了。所以可以淺拷貝,可以析構,如果你還要用里面的值的話,就會有問題。
- doing std::move() on an lvalue reference is bad!!! Code producing an lvalue reference expects the object to remain valid. But code receiving an rvalue reference expects to be able to steal from it. This leaves you with a reference pointing to a potentially-invalid object.
- 一個例外是當函數參數類型是到模板形參的右值引用(“轉發引用”或“通用引用”)時,該情況下轉而使用 std::forward
使用move的優點
move用于移動構造時,可以使移動構造函數成為淺拷貝,由于rvalue出去也沒有引用了,所以很安全,性能也比深拷貝要好
#include <cstdio> #include <cstring> #include <vector>class MyType {public:MyType() {pointer_ = new int;*pointer_ = 1;memset(array_, 0, sizeof(array_));vector_.push_back(3.14);}MyType(MyType&& other) {fprintf(stderr, "move ctor\n");// Steal the memory, null out |other|.// 因為other會被析構,所以要把它賦值為null,之所以敢這么做,因為傳進來的是個右值,外面沒有人用了。pointer_ = other.pointer_;other.pointer_ = nullptr;// Copy the contents of the array.memcpy(array_, other.array_, sizeof(array_));// Swap with our (empty) vector.vector_.swap(other.vector_);}~MyType() {delete pointer_;}private:int* pointer_;char array_[42];std::vector<float> vector_; };void ProcessMyType(MyType type) { }MyType MakeMyType(int a) {if (a % 2) {MyType type;return type;}MyType type;return type; }int main(int argc, char **argv) {// MakeMyType returns an rvalue MyType.// Both lines below call our move constructor.MyType type = MakeMyType(2);ProcessMyType(MakeMyType(2)); }move 左值測試
struct testc {int a; } void test_func(testc a) {}; void test_func_1(testc &&a) {}; int main() {testc lv;testc &a = lv; //左值引用,沒有構造testc &&b = static_cast<testc&&>(lv); // 右值引用,沒有構造testc c = std::move(lv); // 把一個右值引用賦值給一個左值,如果有移動構造函數,則調用移動構造函數。沒有則調用拷貝構造函數test_func(std::move(lv)); // 這個move僅僅是把調用test_func時候創建形參的過程從拷貝構造變成了移動構造。并沒有減多少開銷test_func_1(std::move(lv)); // 沒有構造函數,test_func_1直接對形參進行右值引用testc c1;c1 = std::move(lv); // 這兩句話更慘,首先構造了c1,調用了構造函數,然后調用了operator=(testc &c)賦值c1 = lv; // 同上,是否使用move并沒有帶來額外的開銷。testc &d = std::move(lv);testc &&e std::move(lv); // 右值引用幅值給右值引用,沒有構造, lv仍然可以對結構體進行操作testc f = lv; // 拷貝構造函數,注意這里沒有調用operator+ }move 右值測試
struct testc {int a;testc (testc &&other) {a = c.a;}// 移動構造還有一種寫法testc(testc&& other) {// Note that although the type of |other| is an rvalue reference,// |other| itself is an lvalue, since it is a named object. In order// to ensure that the move assignment is used, we have to explicitly// specify std::move(other).*this = std::move(other);} } testc rv() { return testc(); } testc rv(int a) {// This is here to circumvent some compiler optimizations,// to ensure that we will actually call a move constructor.// 這點比較精髓,如果沒有這個wrapper函數,可能所有的左值右值的構造都會被編譯器優化掉if (a % 2) {testc c;return c;}testc c;return c; }void receiver(testc &&a) {... }int main() {testc lv; // 構造函數lv = rv(2); // rv里面會調用構造函數,構造一個臨時右值,然后移動構造到一個臨時變量(第一個臨時變量析構),然后這個臨時變量再調用operator=復制,然后rv的臨時右值析構(第二個臨時變量析構)testc a = rv(); // rv里面調用構造函數,然后a就會直接用這個對象,這中間沒有任何構造析構拷貝了,原因是因為編譯器優化掉了。testc a1 = rv(2); // rv里面調用構造函數,然后移動構造!!!(如果沒有定義移動構造函數,那么就用拷貝構造函數)。然后rv(2)出來的臨時右值被析構testc &&b = rv(2); // 同上。rv里面調用構造函數,然后移動構造到一個新的地方,rc里面的析構,b指向的是新拷貝的一塊地方 (由此可見,使用一個右值引用之后,實際上是另外開辟了一個空間存儲,這時這個變量就變成左值了!)testc &&c = static_cast<testc&&>(rv(1)); // 同上 testc &&d = static_cast<testc>(rv(1)); // 同上 testc &&d1 = std::move(lv); // 同上,mv創建一個區域,然后lv移動構造過去,持有的是那片空間testc &&e = std::move(rv(2)); // 同上,但是感覺move那個地方還會有一層析構,并且程序結束之后就不會析構了,不要這么做,rv(2)已經是右值了receiver(rv()); // receiver的形參成為rv()里的臨時變量,然后形參就退化成右值了!!!receiver(move(lv)); // 這樣就沒有任何構造析構了 }注意
參考鏈接
總結
以上是生活随笔為你收集整理的C++右值引用和完美转发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++使用StringPiece减少st
- 下一篇: io_uring设计理念及使用方式总结