1. 程式人生 > >04區分通用引用和右值引用 Scott Meyers的effective modern c++講座摘要

04區分通用引用和右值引用 Scott Meyers的effective modern c++講座摘要

.com all temp 對象 變量 圖片 做了 oca png

大師就是大師,可愛的蘑菇頭,講的太好了,很精彩!有激情,有耐心,很難解釋的東西隨手舉幾個例子就明白了!強烈推薦看看視頻!
https://www.youtube.com/playlist?list=PLmxXlAVb5hkyq5njldMEPYdOqTAQPLChR

技術分享圖片

核心問題

  • 右值引用 一定是 type&&
  • type&& 不一定是 右值引用(還有可能是通用引用)

先來看看,type&&的雙面性

void f(Widget&& param);        // 右值引用
Widget&& var1 = Widget();      // 右值引用

auto&& var2 = var1;            // 不是右值引用!!!

decltype(var1) var3 = var1;    // 無法編譯通過
Widget&& var4 = var1;          // 無法編譯通過

template<typename T>           
void f(std::vector<T>&& param); // 右值引用

template<typename T>
void f(T&& param);              // 不是右值引用!!!

在這裏,"type&&"中的"&&"意味著:

  • 右值引用
     * 綁定右值(Binds rvalues only.)
     * 促進移動(Facilitates moves.)
  • 通用引用
    • 右值引用或左值引用(type&& 有可能是 type& 或 type&& )
    • 綁定所有值,不管是左值,右值,const, 非const...
    • 促進拷貝,也促進移動(May facilitate copies, may facilitate moves)
    • 與轉發引用(Forwarding Reference)相同

如何區分,簡單來說

  • 如果一個變量或參數的聲明類型是T&&,並且需要推導出類型T, 這就是通用引用,否則就是右值引用
  • 通用引用是需要初始化的,如果是左值,那就是左值引用,如果是右值,那就是右值引用

下面主要通過示例來解釋

template<typename T>
void f(T&& param)    // 典型的通用引用,T&& + 需要推導類型

Widget w;            
f(w);                // w是左值,通用引用就是左值引用,f(Widget&)

f(std::move(w));     // std::move無條件轉換為右值,所以通用引用就是右值引用,f(Widget&&)
f(Widget());         // Widget()是右值,這也是右值引用,f(Widget&&)

std::vector<int> v;
...
auto&& val = 10;       // 10是右值,所以通用引用就是右值引用,val的類型就是int&&

auto&& element = v[5]; // v[5]返回的是int&, 所以這裏是左值引用,element的類型是int&
// 有類型推導才能是通用引用
void f(Widget&& w);          // 沒有類型推導,所以這裏不是通用引用

template<typename T>
void f(T&& param);           // 有類型推導,所以是通用引用

template<typename T>
class Gadget1 {
    Gadget1(Gadget1&& rhs);  // 沒有類型推導,不是通用引用
};

template<typename T1>
class Gadget2 {
    tempalte<typename T2>
    Gadget2(T2&& rhs);       // 有類型推導,是通用引用
};

// 但是不是所有的T&&都是通用引用
template<class T,
         class Allocator=allocator<T>>
class vector{
public:
    ...
    // 這裏是右值引用!!! 因為T是從vecotr<T>來的,而不是通過push_back的參數傳遞來的
    // 我解釋一下,這個T實際上是創建vector時就已經確定了,而不是等調用push_back時才做的類型推導
    void push_back(T&& x);    
    ...
};

std::vector<int> vi;
...
vi.push_back(10.5);    // 實參類型是double, 但形參類型是int&&

Scott在這裏解釋的比較多,就是push_back為什麽要有一個通用引用的參數?

過去vector的push_back實際上會構造一個int, 然後放到vector中,這會導致效率問題,如果構造的是一個很大的對象,那麽每次調用push_back時都會調用拷貝構造函數,那麽就會導致效率直線下降,這也是造成過去標準庫讓人詬病的原因。

而現在引入右值引用,上面說過,是用於促進移動義的,因為10.5是一個右值,那麽在放到vector中時就會直接移動這個右值,而不會再構造一個int!想像一下,如果是一個大對象,那麽就會直接移動這個大對象,而不需要再調用拷貝構造函數,效率大大提升!

vector<BigObject> vi;

BigObject o;

vi.push_back(o);               // 這是還是會調用BigObject的拷貝構造函數

vi.push_back(std::move(0));    // std::move無條件轉換為右值,這裏不會再調用拷貝構造函數,而是直接移動
vi.push_back(BigObject());     // 這裏也是右值,同上,不調用拷貝構造函數

最後比較一下push_back和emplace_back

從名稱來看,push就是推到最後,有點不論怎麽拿,各種暴力搬運,反正擱在最後就行了;而emplace就是放到最後,有點輕拿輕放的感覺。現在一般建議新代碼直接使用emplace_back。

template<class T, class Allocator=allocator<T>>
class vector{
public:
    ...
    void push_back(const T& x);         // 左值引用,拷貝值
    void push_back(T&& x);              // 右值引用,移動值
    
    template<class... Args>
    void emplace_back(Args&&... args);  // 通用引用,轉發所有值
    ...
}

現在實現中,兩個push_back內部都是直接調用emplace_back, emplace_back是通用引用,所以可以接收任何值,無論左值還是右值。emplace_back使用變長參數,好處就是使用更加方便,比如:

class Widget
{
public:
    Widget(int a, int b);
};

std::vector<Widget> v;

Widget w;

v.emplace_back(w);               // 左值,拷貝值

v.emplace_back(std::move(w));    // 右值,移動值
v.emplace(Widget(10, 10));       // 右值,移動值
v.emplace(10, 10);               // 右值,移動值,這裏使用了變長參數直接調用了構造函數,而push_back就不允許這樣做了

04區分通用引用和右值引用 Scott Meyers的effective modern c++講座摘要