Google C++ Coding Style:右值引用(Rvalue Reference)

Horky發表於2015-08-04

右值引用是一個C++11特性,標記為T&&。GSG中定義:只為移動建構函式(Move constructor)和移動賦值操作(Move assignment)使用右值引用。並且不要使用std::Forward(提供的完美轉發特性)。

C++中右值指表示式結束時就不再存的臨時物件。在C++11中,右值分為純右值(即原始字面量,表示式產生的臨時變數等),以及一個將亡值(expiring value, 使用<<深入應用C++11>>中的譯法,指的是與右值引用相關的表示式,如將被移動的物件,T&&函式返回值等)。

以函式返回值表達不出右值引用的威力,因為編譯的本身的優化會解決不必要的物件複製操作。而作為函式引數,如果使用const T&之類的形式也能夠有效避免不必要的物件拷貝。這裡特別以與標準容器配合,體現一下,右值引用最大的價值:避免深拷貝。

// 下面一個完整提供了Move Constructor和Move assignment的類。
#include <iostream>
#include <string>
#include <vector>
#include <string.h>

class Foo {
 private:
    int x = 0;
    int y = 0;
    char* strPtr = nullptr;
 public:
    Foo() {
        std::cout << "Constructor was called." << std::endl;
    }

    Foo(const char* s) {
        std::cout << "Constructor with string:" << s << std::endl;
        if (s != nullptr) {
          strPtr = new char[strlen(s)];
          strcpy(strPtr, s);
        }
    }

    // Copy constructor
    Foo(const Foo& a) : x(a.x),
                        y(a.y) {
        // Deep copy
        copyStringValue(a.strPtr);
        std::cout << "Copy constructor was called." << std::endl;
    }

    // Move constructor, no need copy string in deep.
    Foo(Foo&& a) : x(a.x),
                   y(a.y),
                   strPtr(a.strPtr) {
        a.strPtr = nullptr;  // 注意要清掉之前的字串,這樣才是移動。
        std::cout << "Move constructor was called." << std::endl;
    }

    Foo& operator=(const Foo& a) {
        x = a.x;
        y = a.y;
        copyStringValue(a.strPtr);
        std::cout << "Assignment Operator was called." << std::endl;
    }

    ~Foo() {
        if (strPtr != nullptr) {
            std::cout << "Free allocated string:" << strPtr << std::endl;
            delete strPtr;
        }
        std:: cout << "Deconstructor was called." << std::endl;
    }

 private:
    void copyStringValue(const char* s) {
        if (strPtr != nullptr) {
            delete strPtr;
            strPtr = nullptr;
        }

        if (s != nullptr) {
            strPtr = new char[strlen(s)];
            strcpy(strPtr, s);
        }
    }
};

int main(void) {
    {
      std::cout << "Need to clear string twice:" << std::endl;
      std::vector<Foo> myVec;
      Foo a("Instance A");
      myVec.push_back(a);
    }

    std::cout << "============" << std::endl;

    {
      std::cout << "Only need to clear string one time:" << std::endl;
      std::vector<Foo> myVec;
      Foo c("Instance C");
      myVec.push_back(std::move(c));
    }

    std::cout << "============" << std::endl;
    {
        Foo d("Instance D");
        Foo x = d;
    }

    std::cout << "============" << std::endl;
    {
        Foo e("Instance E");
        Foo&& y = std::move(e);
    }
}

觀察程式碼刪除字串的次數,就可以瞭解右值引用的作用了。程式執行的輸出如下:

Need to clear string twice:
Constructor with string:Instance A
Copy constructor was called.
Free allocated string:Instance A
Deconstructor was called.
Free allocated string:Instance A
Deconstructor was called.
============
Only need to clear string one time:
Constructor with string:Instance C
Move constructor was called.
Deconstructor was called.
Free allocated string:Instance C
Deconstructor was called.
============
Constructor with string:Instance D
Copy constructor was called.
Free allocated string:Instance D
Deconstructor was called.
Free allocated string:Instance D
Deconstructor was called.
============
Constructor with string:Instance E
Free allocated string:Instance E
Deconstructor was called.

但是考慮到右值引用中的引用摺疊(reference collapsing)會引入一些複雜度(左右值的轉換規則),造成理解上的問題,所以將右值引用的應用範圍做了如開篇所說的限定。

在實際應用中,會出現沒有直接定義型別的右值引用,被稱為universal reference,需要進行型別推導。另一種情況是使用auto &&定義的也是universal reference。

關於std::forward,它被稱為完美轉發(Perfect Forwarding)。要解決的問題是在函式模板中,完全依照模板的引數的型別,保持引數的左值,右值特徵),將引數傳遞給函式模板中呼叫的另一個函式(轉自<<深入應用C++11>>)。根據這個定義,完美轉發僅針對需要呼叫內部實現的模板函式,而且需要開發者識別出哪些情況是有效的,而哪些情況下又是無效的。比如適用的場景:

template<class T>
void foo(T&& arg) 
{
  // 如下保持arg的型別傳入到bar()中
  bar(std::forward<T>(arg));
}

但如果內部函式無需要針對左值或右值做特殊處理,這種場景是不需要轉發的。參考:When not to use std::forward

相關文章