C++11 中的右值引用與轉移語義

發表於2016-07-20

本文介紹了 C++11 標準中的一個特性,右值引用和轉移語義。這個特效能夠使程式碼更加簡潔高效。

新特性的目的

右值引用 (Rvalue Referene) 是 C++ 新標準 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它實現了轉移語義 (Move Sementics) 和精確傳遞 (Perfect Forwarding)。它的主要目的有兩個方面:

  1. 消除兩個物件互動時不必要的物件拷貝,節省運算儲存資源,提高效率。
  2. 能夠更簡潔明確地定義泛型函式。

左值與右值的定義

C++( 包括 C) 中所有的表示式和變數要麼是左值,要麼是右值。通俗的左值的定義就是非臨時物件,那些可以在多條語句中使用的物件。所有的變數都滿足這個定義,在多條程式碼中都可以使用,都是左值。右值是指臨時的物件,它們只在當前的語句中有效。請看下列示例 :

  1. 簡單的賦值語句

    在這條語句中,i 是左值,0 是臨時值,就是右值。在下面的程式碼中,i 可以被引用,0 就不可以了。立即數都是右值。

  2. 右值也可以出現在賦值表示式的左邊,但是不能作為賦值的物件,因為右值只在當前語句有效,賦值沒有意義。如:((i>0) ? i : j) = 1;在這個例子中,0 作為右值出現在了”=”的左邊。但是賦值物件是 i 或者 j,都是左值。在 C++11 之前,右值是不能被引用的,最大限度就是用常量引用繫結一個右值,如 :

    在這種情況下,右值不能被修改的。但是實際上右值是可以被修改的,如 :

    T 是一個類,set 是一個函式為 T 中的一個變數賦值,get 用來取出這個變數的值。在這句中,T() 生成一個臨時物件,就是右值,set() 修改了變數的值,也就修改了這個右值。

    既然右值可以被修改,那麼就可以實現右值引用。右值引用能夠方便地解決實際工程中的問題,實現非常有吸引力的解決方案。

左值和右值的語法符號

左值的宣告符號為”&”, 為了和左值區分,右值的宣告符號為”&&”。

示例程式 :

執行結果 :

Process_value 函式被過載,分別接受左值和右值。由輸出結果可以看出,臨時物件是作為右值處理的。

但是如果臨時物件通過一個接受右值的函式傳遞給另一個函式時,就會變成左值,因為這個臨時物件在傳遞過程中,變成了命名物件。

示例程式 :

執行結果 :

雖然 2 這個立即數在函式 forward_value 接收時是右值,但到了 process_value 接收時,變成了左值。

轉移語義的定義

右值引用是用來支援轉移語義的。轉移語義可以將資源 ( 堆,系統物件等 ) 從一個物件轉移到另一個物件,這樣能夠減少不必要的臨時物件的建立、拷貝以及銷燬,能夠大幅度提高 C++ 應用程式的效能。臨時物件的維護 ( 建立和銷燬 ) 對效能有嚴重影響。

轉移語義是和拷貝語義相對的,可以類比檔案的剪下與拷貝,當我們將檔案從一個目錄拷貝到另一個目錄時,速度比剪下慢很多。

通過轉移語義,臨時物件中的資源能夠轉移其它的物件裡。

在現有的 C++ 機制中,我們可以定義拷貝建構函式和賦值函式。要實現轉移語義,需要定義轉移建構函式,還可以定義轉移賦值操作符。對於右值的拷貝和賦值會呼叫轉移建構函式和轉移賦值操作符。如果轉移建構函式和轉移拷貝操作符沒有定義,那麼就遵循現有的機制,拷貝建構函式和賦值操作符會被呼叫。

普通的函式和操作符也可以利用右值引用操作符實現轉移語義。

實現轉移建構函式和轉移賦值函式

以一個簡單的 string 類為示例,實現拷貝建構函式和拷貝賦值操作符。

示例程式 :

執行結果 :

這個 string 類已經基本滿足我們演示的需要。在 main 函式中,實現了呼叫拷貝建構函式的操作和拷貝賦值操作符的操作。

MyString(“Hello”) 和 MyString(“World”) 都是臨時物件,也就是右值。雖然它們是臨時的,但程式仍然呼叫了拷貝構造和拷貝賦值,造成了沒有意義的資源申請和釋放的操作。如果能夠直接使用臨時物件已經申請的資源,既能節省資源,有能節省資源申請和釋放的時間。這正是定義轉移語義的目的。

我們先定義轉移建構函式。

和拷貝建構函式類似,有幾點需要注意:

1. 引數(右值)的符號必須是右值引用符號,即“&&”。

2. 引數(右值)不可以是常量,因為我們需要修改右值。

3. 引數(右值)的資源連結和標記必須修改。否則,右值的解構函式就會釋放資源。轉移到新物件的資源也就無效了。

現在我們定義轉移賦值操作符。

這裡需要注意的問題和轉移建構函式是一樣的。

增加了轉移建構函式和轉移複製操作符後,我們的程式執行結果為 :

由此看出,編譯器區分了左值和右值,對右值呼叫了轉移建構函式和轉移賦值操作符。節省了資源,提高了程式執行的效率。

有了右值引用和轉移語義,我們在設計和實現類時,對於需要動態申請大量資源的類,應該設計轉移建構函式和轉移賦值函式,以提高應用程式的效率。

標準庫函式 std::move

既然編譯器只對右值引用才能呼叫轉移建構函式和轉移賦值函式,而所有命名物件都只能是左值引用,如果已知一個命名物件不再被使用而想對它呼叫轉移建構函式和轉移賦值函式,也就是把一個左值引用當做右值引用來使用,怎麼做呢?標準庫提供了函式 std::move,這個函式以非常簡單的方式將左值引用轉換為右值引用。

示例程式 :

執行結果 :

std::move在提高 swap 函式的的效能上非常有幫助,一般來說,swap函式的通用定義如下:

有了 std::move,swap 函式的定義變為 :

通過 std::move,一個簡單的 swap 函式就避免了 3 次不必要的拷貝操作。

精確傳遞 (Perfect Forwarding)

本文采用精確傳遞表達這個意思。”Perfect Forwarding”也被翻譯成完美轉發,精準轉發等,說的都是一個意思。

精確傳遞適用於這樣的場景:需要將一組引數原封不動的傳遞給另一個函式。

“原封不動”不僅僅是引數的值不變,在 C++ 中,除了引數值之外,還有一下兩組屬性:

左值/右值和 const/non-const。 精確傳遞就是在引數傳遞過程中,所有這些屬性和引數值都不能改變。在泛型函式中,這樣的需求非常普遍。

下面舉例說明。函式 forward_value 是一個泛型函式,它將一個引數傳遞給另一個函式 process_value。

forward_value 的定義為:

函式 forward_value 為每一個引數必須過載兩種型別,T& 和 const T&,否則,下面四種不同型別引數的呼叫中就不能同時滿足  :

對於一個引數就要過載兩次,也就是函式過載的次數和引數的個數是一個正比的關係。這個函式的定義次數對於程式設計師來說,是非常低效的。我們看看右值引用如何幫助我們解決這個問題  :

只需要定義一次,接受一個右值引用的引數,就能夠將所有的引數型別原封不動的傳遞給目標函式。四種不用型別引數的呼叫都能滿足,引數的左右值屬性和 const/non-cosnt 屬性完全傳遞給目標函式 process_value。這個解決方案不是簡潔優雅嗎?

C++11 中定義的 T&& 的推導規則為:

右值實參為右值引用,左值實參仍然為左值引用。

一句話,就是引數的屬性不變。這樣也就完美的實現了引數的完整傳遞。

右值引用,表面上看只是增加了一個引用符號,但它對 C++ 軟體設計和類庫的設計有非常大的影響。它既能簡化程式碼,又能提高程式執行效率。每一個 C++ 軟體設計師和程式設計師都應該理解並能夠應用它。我們在設計類的時候如果有動態申請的資源,也應該設計轉移建構函式和轉移拷貝函式。在設計類庫時,還應該考慮 std::move 的使用場景並積極使用它。

總結

右值引用和轉移語義是 C++ 11 標準中的一個重要特性。每一個專業的 C++ 開發人員都應該掌握並應用到實際專案中。在有機會重構程式碼時,也應該思考是否可以應用新也行。在使用之前,需要檢查一下編譯器的支援情況。

相關文章