1. 程式人生 > >std::move,std::forward與左右值引用

std::move,std::forward與左右值引用

在講這個之前, 必須得講一下左值和右值, 這個知識真的是很冷門又冷門, 如果不是C++11的std::move, 我想我八輩子都不會知道這是什麼東西, 晦澀難懂

左值

簡單的來說就是賦值符號左邊的值, 準確的來說, 左值就是表示式執行後依然存在的持久的物件

右值

右邊的值,表示式執行過後就不再存在的臨時物件, 但是C++肯定是要把程式設計搞得更復雜的, 所以右值又有純右值, 和將亡值
純右值: 純粹的右值要麼是純粹的的字面量, 比如2233, true;要麼是求值結果相當於字面量或者是匿名臨時物件, 比如一個加法2*3,非引用返回的臨時變數, 運算表示式的臨時變數, 原始字面量,lambda表示式都是純右值
將亡值: 就是可以被移動並且將要被銷燬的值. 這個說得有點抽象, 看個程式碼

std::vector<int> foo(){
    std::vector<int> temp={1,2,3,4};
    return temp;
}

int main(int argc,char *argv[]){
    auto res=foo();
    return 0;
}

現在這個寫法相信大家肯定都看過了, 但是這個存在一個問題, foo的返回值在這個裡面作為一個臨時的物件被返回, 這個賦值語句做了什麼呢, 他把這個臨時物件拷貝一遍, 這個返回的物件或者值事實上就是一個右值而且是個純右值, 因為他是表示式求值後得到的值, 如果這個foo()

特別的大, 系統產生的開銷是很龐大的,還有很多這樣的情況,比如vector的增長, 如果vector的capacity不夠用的時候, 它就會產生拷貝的行為, 看一段官方英文的文件

Rvalue references are a new reference type introduced in C++0x that help solve the problem of unncessatry copying and enable perfect forwarding. When the right-hand saide of an assignment is an rvalue, then the left-side object can steal resources from the right-hand side object rather than proforming a separate allocation, thus enabling move semantics.
我的翻譯是這樣的:
右值引用是一種新型的引用, 在C++0x十分被推薦使用, 它用來解決一個問題, 這個問題是關於如何避免不必要的拷貝和更好的進行forward, 當一個右值需要賦值到左值的時候, 左值可以去“竊取”右值的資源, 而且還並不需要開闢一個新的空間,

我們大概理解了這個東西, 大概就是為了防止比如vector insert的時候去拷貝臨時物件, 而是去指向他的引用, 這個是個好東西, 可以防止吃記憶體, 我們也可以大膽猜測, 事實上右值引用就是指標的淺拷貝.
但是關於我找到了這樣的兩個程式碼, 來看這個程式碼:

int main() {
    std::string s1("Hello");
    std::string s2("World");
    s1+s2=s2;
    std::cout<<"s1:"<<s1<<std::endl;
    std::cout<<"s2:"<<s2<<std::endl;
    std::string()="World";
}

超屌的, 這段程式碼居然是可以編譯的, 而且還可以執行, s1+s2很明顯他是一個右值, 但是卻被放在了左邊, std::string()是個臨時物件竟然可以賦值, 類似的程式碼還有

void complex_test(){
    std::complex<int> c1(3,8),c2(1,0);
    c1+c2=std::complex<int>(6,9);
    std::cout<<c1<<" "<<c2<<std::endl;
    std::complex<int>() =std::complex<int> (59,1);
}

這似乎是標準庫的bug, 但是這樣做確實是不被允許的
現在有了右值我們就有了另一種程式碼, 我們在向某個函式傳入臨時物件的時候, 如果這個物件後面不會再使用我們就可以傳入他的右值引用, 這個過程不回去呼叫它的拷貝建構函式,而是呼叫他的引用建構函式,

//現在我們有兩套拷貝的建構函式
//這是vector的iterator
iterator 
insert(const_iterator __position,const value_type& __x);

iterator(const_iterator __position,value_type&& __x){
    return emplace(__ position,std::move(__x));
}

那麼, 我有什麼辦法來進行使用呢?

std::move

#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
    std::string str = "Hello";
    std::vector<std::string> v;
    //呼叫常規的拷貝建構函式,新建字元陣列,拷貝資料
    v.insert(v.end(),str);
    std::cout << "After copy, str is \"" << str << "\"\n";
    //呼叫移動建構函式,掏空str,掏空後,最好不要使用str
    v.insert(v.end(),std::move(str));
    std::cout << "After move, str is \"" << str << "\"\n";
    std::cout << "The contents of the vector are \"" << v[0]
                                         << "\", \"" << v[1] << "\"\n";
}

這裡用到了std::move, 在C++11後就有了這個東西, 它可以把左值強制轉化為右值, 你可以解釋它為一個cast, 以前我們如果要在C++裡面讓一個指標搬家, 我們需要先把它拷貝, 再把它清空,讓它指向空, 現在不用了, 現在你可以移動(竊取)他.
所以我們有了

左值引用和右值引用

直接看程式碼

#include <iostream>
#include <string>

void reference(std::string& str) {
    std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {
    std::cout << "右值" << std::endl;
}

int main()
{
    std::string  lv1 = "string,";       // lv1 是一個左值
    // std::string&& r1 = s1;           // 非法, 右值引用不能引用左值
    std::string&& rv1 = std::move(lv1); // 合法, std::move可以將左值轉移為右值
    std::cout << rv1 << std::endl;      // string,

    const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能夠延長臨時變數的申明週期
    // lv2 += "Test";                   // 非法, 引用的右值無法被修改
    std::cout << lv2 << std::endl;      // string,string

    std::string&& rv2 = lv1 + lv2;      // 合法, 右值引用延長臨時物件宣告週期
    rv2 += "Test";                      // 合法, 非常量引用能夠修改臨時變數
    std::cout << rv2 << std::endl;      // string,string,string,

    reference(rv2);                     // 輸出左值

    return 0;
}

完美轉發 std::forward

如果我們宣告一個右值引用, 那麼這個右值引用本生其實上是一個左值對吧, 看程式碼

void reference(int& v) {
    std::cout << "左值" << std::endl;
}
void reference(int&& v) {
    std::cout << "右值" << std::endl;
}
template <typename T>
void pass(T&& v) {
    std::cout << "普通傳參:";
    reference(v);   // 始終呼叫 reference(int& )
}
int main() {
    std::cout << "傳遞右值:" << std::endl;
    pass(1);        // 1是右值, 但輸出左值

    std::cout << "傳遞左值:" << std::endl;    
    int v = 1;
    pass(v);        // r 是左引用, 輸出左值, 因為編譯器幫我們優化了這一步,上面並不是一個右值的引用, 而是一個引用的引用

    return 0;
}

對於 pass(1) 來說,雖然傳遞的是右值,但由於 v 是一個引用,所以同時也是左值。因此 reference(v) 會呼叫 reference(int&),輸出『左值』。而對於pass(v)而言,v是一個左值,為什麼會成功傳遞給 pass(T&&) 呢?
其實上述的程式碼都還有問題, 那就是事實上如果不用模版函式還會有錯誤

void check_lr(int &value){
    std::cout<<"This is a lvalue"<<std::endl;
}

void check_lr(int &&value){
    std::cout<<"This is a rvalue"<<std::endl;
}

void do_check(int&& value){
    check_lr(value);
}


int main() {

    int x=520;
    check_lr(x); //binggo

    check_lr(1); //bingo

    check_lr(std::move(x));//bingo

    do_check(520); //為什麼到了check裡面520變成了一個左值,(事實上他變成了一個named object),這個叫imperfect forward!

    do_check(std::move(x));//為什麼你也成了左值

    //do_check(x); //編譯都過不了 一個右值引用的形參怎麼可以接受一個左值呢
    //所以上面那個template的pass是編譯器替我們做的優化, 進行了型別的推導, 儘可能的可以編譯, 它強行把我們傳入的v作為一個左值的引用,這個叫引用坍縮規則


    return 0;
}

引用坍縮規則

這是基於引用坍縮規則的:在傳統 C++ 中,我們不能夠對一個引用型別繼續進行引用,但 C++ 由於右值引用的出現而放寬了這一做法,從而產生了引用坍縮規則,允許我們對引用進行引用,既能左引用,又能右引用。但是卻遵循如下規則:傳參表
因此,模板函式中使用 T&& 不一定能進行右值引用,當傳入左值時,此函式的引用將被推導為左值。更準確的講,無論模板引數是什麼型別的引用,當且僅當實參型別為右引用時,模板引數才能被推導為右引用型別。這才使得 v 作為左值的成功傳遞。
完美轉發就是基於上述規律產生的。所謂完美轉發,就是為了讓我們在傳遞引數的時候,保持原來的引數型別(左引用保持左引用,右引用保持右引用)。為了解決這個問題,我們應該使用 std::forward 來進行引數的轉發(傳遞):

#include <iostream>
#include <utility>
void reference(int& v) {
    std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
    std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v) {
    std::cout << "普通傳參:";
    reference(v);
    std::cout << "std::move 傳參:";
    reference(std::move(v));
    std::cout << "std::forward 傳參:";
    reference(std::forward<T>(v));

}
int main() {
    std::cout << "傳遞右值:" << std::endl;
    pass(1);

    std::cout << "傳遞左值:" << std::endl;
    int v = 1;
    pass(v);

    return 0;
}