程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 右值引用

右值引用

編輯:C++入門知識

1、右值引用引入的背景

臨時對象的產生和拷貝所帶來的效率折損,一直是C++所為人诟病的問題。但是C++標准允許編譯器對於臨時對象的產生具有完全的自由度,從而發展出了Copy Elision、RVO(包括NRVO)等編譯器優化技術,它們可以防止某些情況下臨時對象產生和拷貝。下面簡單地介紹一下Copy Elision、RVO,對此不感興趣的可以直接跳過:

(1) Copy Elision

 Copy Elision技術是為了防止某些不必要的臨時對象產生和拷貝,例如:

struct A {
    A(int) {}
    A(const A &) {}
};
A a = 42;

理論上講,上述A a = 42;語句將分三步操作:第一步由42構造一個A類型的臨時對象,第二步以臨時對象為參數拷貝構造a,第三步析構臨時對象。如果A是一個很大的類,那麼它的臨時對象的構造和析構將造成很大的內存開銷。我們只需要一個對象a,為什麼不直接以42為參數直接構造a呢?Copy Elision技術正是做了這一優化。

【說明】:你可以在A的拷貝構造函數中加一打印語句,看有沒有調用,如果沒有被調用,那麼恭喜你,你的編譯器支持Copy Elision。但是需要說明的是:A的拷貝構造函數雖然沒有被調用,但是它的實現不能沒有訪問權限,不信你將它放在private權限裡試試,編譯器肯定會報錯。

(2) 返回值優化(RVO,Return Value Optimization)

 返回值優化技術也是為了防止某些不必要的臨時對象產生和拷貝,例如:

struct A {
    A(int) {}
    A(const A &) {}
};
A get() {return A(1);}
A a = get();

理論上講,上述A a = get();語句將分別執行:首先get()函數中創建臨時對象(假設為tmp1),然後以tmp1為參數拷貝構造返回值(假設為tmp2),最後再以tmp2為參數拷貝構造a,其中還伴隨著tmp1和tmp2的析構。如果A是一個很大的類,那麼它的臨時對象的構造和析構將造成很大的內存開銷。返回值優化技術正是用來解決此問題的,它可以避免tmp1和tmp2兩個臨時對象的產生和拷貝。

【說明】: a)你可以在A的拷貝構造函數中加一打印語句,看有沒有調用,如果沒有被調用,那麼恭喜你,你的編譯器支持返回值優化。但是需要說明的是:A的拷貝構造函數雖然沒有被調用,但是它的實現不能沒有訪問權限,不信你將它放在private權限裡試試,編譯器肯定會報錯。

b)除了返回值優化,你可能還聽說過一個叫具名返回值優化(Named Return Value Optimization,NRVO)的優化技術,從程序員的角度而言,它其實跟RVO同樣的邏輯。只是它的臨時對象具有變量名標識,例如修改上述get()函數為:

A get() {
    A tmp(1); // #1
    // do something
    return tmp;
}
A a = get(); // #2

想想上述修改後A類型共有幾次對象構造?雖然#1處看起來有一次顯示地構造,#2處看起來也有一次顯示地構造,但如果你的編譯器支持NRVO和Copy Elision,你會發現整個A a = get();語句的執行過程,只有一次A對象的構造。如果你在get()函數return語句前打印tmp變量的地址,在A a = get();語句後打印a的地址,你會發現兩者地址相同,這就是應用了NRVO技術的結果。

(3) Copy Elision、RVO無法避免的臨時對象的產生和拷貝

雖然Copy Elision和NVO(包括NRVO)等技術能避免一些臨時對象的產生和拷貝,但某些情況下它們卻發揮不了作用,例如:

template <typename T>
void swap(T& a, T& b) {
    T tmp(a);
    a = b;
    b = tmp;
}

我們只是想交換a和b兩個對象所擁有的數據,但卻不得不使用一個臨時對象tmp備份其中一個對象,如果T類型對象擁有指向(或引用)從堆內存分配的數據,那麼深拷貝所帶來的內存開銷是可以想象的。為此,C++11標准引入了右值引用,使用它可以使臨時對象的拷貝具有move語意,從而可以使臨時對象的拷貝具有淺拷貝般的效率,這樣便可以從一定程度上解決臨時對象的深度拷貝所帶來的效率折損。

 

2、C++03標准中的左值與右值

要理解右值引用,首先得區分左值(lvalue)和右值(rvalue)。

C++03標准中將表達式分為左值和右值,並且“非左即右”:

    Every expression is either an lvalue or an rvalue.

區分一個表達式是左值還是右值,最簡便的方法就是看能不能夠對它取地址:如果能,就是左值;否則,就是右值。

【說明】:由於右值引用的引入,C++11標准中對表達式的分類不再是“非左即右”那麼簡單,不過為了簡單地理解,我們暫時只需區分左值右值即可,C++11標准中的分類後面會有描述。

 

3、右值引用的綁定規則

右值引用(rvalue reference,&&)跟傳統意義上的引用(reference,&)很相似,為了更好地區分它們倆,傳統意義上的引用又被稱為左值引用(lvalue reference)。下面簡單地總結了左值引用和右值引用的綁定規則(函數類型對象會有所例外):

(1)非const左值引用只能綁定到非const左值;
(2)const左值引用可綁定到const左值、非const左值、const右值、非const右值;
(3)非const右值引用只能綁定到非const右值;
(4)const右值引用可綁定到const右值和非const右值。

測試例子如下:

復制代碼
struct A { A(){} };
A lvalue;                             // 非const左值對象
const A const_lvalue;                 // const左值對象
A rvalue() {return A();}              // 返回一個非const右值對象
const A const_rvalue() {return A();}  // 返回一個const右值對象

// 規則一:非const左值引用只能綁定到非const左值
A &lvalue_reference1 = lvalue;         // ok
A &lvalue_reference2 = const_lvalue;   // error
A &lvalue_reference3 = rvalue();       // error
A &lvalue_reference4 = const_rvalue(); // error

// 規則二:const左值引用可綁定到const左值、非const左值、const右值、非const右值
const A &const_lvalue_reference1 = lvalue;         // ok
const A &const_lvalue_reference2 = const_lvalue;   // ok
const A &const_lvalue_reference3 = rvalue();       // ok
const A &const_lvalue_reference4 = const_rvalue(); // ok

// 規則三:非const右值引用只能綁定到非const右值
A &&rvalue_reference1 = lvalue;         // error
A &&rvalue_reference2 = const_lvalue;   // error
A &&rvalue_reference3 = rvalue();       // ok
A &&rvalue_reference4 = const_rvalue(); // error

// 規則四:const右值引用可綁定到const右值和非const右值,不能綁定到左值
const A &&const_rvalue_reference1 = lvalue;         // error
const A &&const_rvalue_reference2 = const_lvalue;   // error
const A &&const_rvalue_reference3 = rvalue();       // ok
const A &&const_rvalue_reference4 = const_rvalue(); // ok

// 規則五:函數類型例外
void fun() {}
typedef decltype(fun) FUN;  // typedef void FUN();
FUN       &  lvalue_reference_to_fun       = fun; // ok
const FUN &  const_lvalue_reference_to_fun = fun; // ok
FUN       && rvalue_reference_to_fun       = fun; // ok
const FUN && const_rvalue_reference_to_fun = fun; // ok
復制代碼

【說明】:(1) 一些支持右值引用但版本較低的編譯器可能會允許右值引用綁定到左值,例如g++4.4.4就允許,但g++4.6.3就不允許了,clang++3.2也不允許,據說VS2010 beta版允許,正式版就不允許了,本人無VS2010環境,沒測試過。

(2)右值引用綁定到字面值常量同樣符合上述規則,例如:int &&rr = 123;,這裡的字面值123雖然被稱為常量,可它的類型為int,而不是const int。對此C++03標准文檔4.4.1節及其腳注中有如下說明:

    If T is a non-class type, the type of the rvalue is the cv-unqualified version of T.
    In C++ class rvalues can have cv-qualified types (because they are objects). This differs from ISO C, in which non-lvalues never have cv-qualified types.

因此123是非const右值,int &&rr = 123;語句符合上述規則三。

 

4、C++11標准中的表達式分類

右值引用的引入,使得C++11標准中對表達式的分類不再是非左值即右值那麼簡單,下圖為C++11標准中對表達式的分類:

簡單解釋如下:

    (1)lvalue仍然是傳統意義上的左值;
    (2)xvalue(eXpiring value)字面意思可理解為生命周期即將結束的值,它是某些涉及到右值引用的表達式的值(An xvalue is the result of certain kinds of expressions involving rvalue references),例如:調用一個返回類型為右值引用的函數的返回值就是xvalue。
    (3)prvalue(pure rvalue)字面意思可理解為純右值,也可認為是傳統意義上的右值,例如臨時對象和字面值等。
    (4)glvalue(generalized value)廣義的左值,包括傳統的左值和xvalue。
    (5)rvalue除了傳統意義上的右值,還包括xvalue。

上述lvalue和prvalue分別跟傳統意義上的左值和右值概念一致,比較明確,而將xvalue描述為『某些涉及到右值引用的表達式的值』,某些是哪些呢?C++11標准給出了四種明確為xvalue的情況:

復制代碼
[ Note: An expression is an xvalue if it is:
  -- the result of calling a function, whether implicitly or explicitly, whose return type is an rvalue reference to object type,
  -- a cast to an rvalue reference to object type,
  -- a class member access expression designating a non-static data member of non-reference type in which the object expression is an xvalue, or
  -- a .* pointer-to-member expression in which the first operand is an xvalue and the second operand is a pointer to data member.
  In general, the effect of this rule is that named rvalue references are treated as lvalues and unnamed rvalue references to objects are treated as xvalues; rvalue references to functions are treated as lvalues whether named or not. --end note ]
[ Example:
    struct A {
        int m;
    };
    A&& operator+(A, A);
    A&& f();
    A a;
    A&& ar = static_cast<A&&>(a);
  The expressions f(), f().m, static_cast<A&&>(a), and a + a are xvalues. The expression ar is an lvalue.
--end example ]
復制代碼

簡單地理解就是:具名的右值引用(named rvalue reference)屬於左值,不具名的右值引用(unamed rvalue reference)就屬於xvalue,而引用函數類型的右值引用不論是否具名都當做左值處理。看個例子更容易理解:

A rvalue(){ return A(); }
A &&rvalue_reference() { return A(); }
fun();              // 返回的是不具名的右值引用,屬於xvalue
A &&ra1 = rvalue(); // ra1是具名右值應用,屬於左值
A &&ra2 = ra1;      // error,ra1被當做左值對待,因此ra2不能綁定到ra1(不符合規則三)
A &la = ra1;        // ok,非const左值引用可綁定到非const左值(符合規則一)

 

5、move語意

現在,我們重新顧到1-(3),其中提到move語意,那麼怎樣才能使臨時對象的拷貝具有move語意呢?下面我們以一個類的實現為例:

復制代碼
class A {
public:
    A(const char *pstr = 0) { m_data = (pstr != 0 ? strcpy(new char[strlen(pstr) + 1], pstr) : 0); }
// copy constructor A(const A &a) { m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0); }
// copy assigment A &operator =(const A &a) { if (this != &a) { delete [] m_data; m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0); } return *this; }
// move constructor A(A &&a) : m_data(a.m_data) { a.m_data = 0; }
// move assigment A & operator = (A &&a) { if (this != &a) { m_data = a.m_data; a.m_data = 0; } return *this; }
~A() { delete [] m_data; }
private: char * m_data; };
復制代碼

從上例可以看到,除了傳統的拷貝構造(copy constructor)和拷貝賦值(copy assigment),我們還為A類的實現添加了移動拷貝構造(move constructor)和移動賦值(move assigment)。這樣,當我們拷貝一個A類的(右值)臨時對象時,就會使用具有move語意的移動拷貝構造函數,從而避免深拷貝中strcpy()函數的調用;當我們將一個A類的(右值)臨時對象賦值給另一個對象時,就會使用具有move語意的移動賦值,從而避免拷貝賦值中strcpy()函數的調用。這就是所謂的move語意。

 

6、std::move()函數的實現

了解了move語意,那麼再來看1-(3)中的效率問題:

template <typename T> // 如果T是class A
void swap(T& a, T& b) {
    T tmp(a);  // 根據右值引用的綁定規則三可知,這裡不會調用move constructor,而會調用copy constructor
    a = b;     // 根據右值引用的綁定規則三可知,這裡不會調用move assigment,而會調用copy assigment
    b = tmp;   // 根據右值引用的綁定規則三可知,這裡不會調用move assigment,而會調用copy assigment
}

從上例可以看到,雖然我們實現了move constructor和move assigment,但是swap()函數的例子中仍然使用的是傳統的copy constructor和copy assigment。要讓它們真正地使用move語意的拷貝和復制,就該std::move()函數登場了,看下面的例子:

void swap(A &a, A &b) {
    A tmp(std::move(a)); // std::move(a)為右值,這裡會調用move constructor
    a = std::move(b);    // std::move(b)為右值,這裡會調用move assigment
    b = std::move(tmp);  // std::move(tmp)為右值,這裡會調用move assigment
}

我們不禁要問:我們通過右值應用的綁定規則三和規則四,知道右值引用不能綁定到左值,可是std::move()函數是如何把上述的左值a、 b和tmp變成右值的呢?這就要從std::move()函數的實現說起,其實std::move()函數的實現非常地簡單,下面以libcxx庫中的實現(在<type_trait>頭文件中)為例:

template <class _Tp>
inline typename remove_reference<_Tp>::type&& move(_Tp&& __t) {
    typedef typename remove_reference<_Tp>::type _Up;
    return static_cast<_Up&&>(__t);
}

其中remove_reference的實現如下:

template <class _Tp> struct remove_reference        {typedef _Tp type;};
template <class _Tp> struct remove_reference<_Tp&>  {typedef _Tp type;};
template <class _Tp> struct remove_reference<_Tp&&> {typedef _Tp type;};

從move()函數的實現可以看到,move()函數的形參(Parameter)類型為右值引用,它怎麼能綁定到作為實參(Argument)的左值a、b和tmp呢?這不是仍然不符合右值應用的綁定規則三嘛!簡單地說,如果move只是個普通的函數(而不是模板函數),那麼根據右值應用的綁定規則三和規則四可知,它的確不能使用左值作為其實參。但它是個模板函數,牽涉到模板參數推導,就有所不同了。C++11標准文檔14.8.2.1節中,關於模板函數參數的推導描述如下:

    Template argument deduction is done by comparing each function template parameter type (call it P) with the type of the corresponding argument of the call (call it A) as described below. (14.8.2.1.1)
    If P is a reference type, the type referred to by P is used for type deduction. If P is an rvalue reference to a cvunqualified template parameter and the argument is an lvalue, the type "lvalue reference to A" is used in place of A for type deduction. (14.8.2.1.3)

大致意思是:模板參數的推導其實就是形參和實參的比較和匹配,如果形參是一個引用類型(如P&),那麼就使用P來做類型推導;如果形參是一個cv-unqualified(沒有const和volatile修飾的)右值引用類型(如P&&),並且實參是一個左值(如類型A的對象),就是用A&來做類型推導(使用A&代替A)。

template <class _Tp> void f(_Tp &&) { /* do something */ }
template <class _Tp> void g(const _Tp &&) { /* do something */ }
int x = 123;
f(x);   // ok,f()模板函數形參為非const非volatile右值引用類型,實參x為int類型左值,使用int&來做參數推導,因此調用f<int &>(int &)
f(456); // ok,實參為右值,調用f<int>(int &&)
g(x);   // error,g()函數模板參數為const右值引用類型,會調用g<int>(const int &&),通過右值引用規則四可知道,const右值引用不能綁定到左值,因此會導致編譯錯誤

了解了模板函數參數的推導過程,已經不難理解std::move()函數的實現了,當使用左值(假設其類型為T)作為參數調用std::move()函數時,實際實例化並調用的是std::move<T&>(T&),而其返回類型T&&,這就是move()函數左值變右值的過程(其實左值本身仍是左值,只是被當做右值對待而已,被人“抄了家”,變得一無所有)。

【說明】: move()函數改名為rval()可能會更好些,但是move()這個名字已經被使用了好些年了(C++FAQ: Maybe it would have been better if move() had been called rval(), but by now move() has been used for years.)。

 

7、完整的示例

至此,我們已經了解了不少右值引用的知識點了,下面給出了一個完整地利用右值引用實現move語意的例子:

復制代碼
#include <iostream>
#include <cstring>

#define PRINT(msg) do { std::cout << msg << std::endl; } while(0)

template <class _Tp> struct remove_reference        {typedef _Tp type;};
template <class _Tp> struct remove_reference<_Tp&>  {typedef _Tp type;};
template <class _Tp> struct remove_reference<_Tp&&> {typedef _Tp type;};

template <class _Tp>
inline typename remove_reference<_Tp>::type&& move(_Tp&& __t) {
    typedef typename remove_reference<_Tp>::type _Up;
    return static_cast<_Up&&>(__t);
}

class A {
public:
    A(const char *pstr) {
        PRINT("constructor");
        m_data = (pstr != 0 ? strcpy(new char[strlen(pstr) + 1], pstr) : 0);
    }
    A(const A &a) {
        PRINT("copy constructor");
        m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0);
    }
    A &operator =(const A &a) {
        PRINT("copy assigment");
        if (this != &a) {
            delete [] m_data;
            m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0);
        }
        return *this;
    }
    A(A &&a) : m_data(a.m_data) {
        PRINT("move constructor");
        a.m_data = 0;
    }
    A & operator = (A &&a) {
        PRINT("move assigment");
        if (this != &a) {
            m_data = a.m_data;
            a.m_data = 0;
        }
return *this;
    }
    ~A() { PRINT("destructor"); delete [] m_data; }
private:
    char * m_data;
};

void swap(A &a, A &b) {
    A tmp(move(a));
    a = move(b);
    b = move(tmp);
}

int main(int argc, char **argv, char **env) {
    A a("123"), b("456");
    swap(a, b);
    return 0;
}
復制代碼

輸出結果為:

復制代碼
constructor
constructor
move constructor
move assigment
move assigment
destructor
destructor
destructor
復制代碼

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved