閱讀856 返回首頁    go 阿裏雲 go 技術社區[雲棲]


C++11中貌似有理的右值

C++11中貌似有理的右值

C++11非常重要的一個概念是引入了右值right value(rvalue)概念,這篇文章不是長篇大論rvalue的文章,而是在我閱讀c++頭文件type_traits時看到一些代碼的由感而發的。

right value顧名思義是右值的意思,估計你能體會到這是“即將消失”的意思,如果你還不明白,可以看這個例子:


Object f() 
{ 
    Object o; return o;
}

int main()
{
    Object obj = f();
}

你可以想象,f()函數返回一個臨時對象值,然後把這個臨時對象copy-assign給main函數中的對象obj,然後f()返回的臨時對象被析構,未經優化的編譯器是上述這個過程,然而現代C++允許返回值優化(RVO),實際上會傳遞obj的指針給f()函數,從而消除從返回值copy到obj過程的。雖然有這種優化,但是右值這個語義依然是存在的。

右值的好處

C++11引入了move概念,當一個右值被識別出來時,意味著傳入的值的內容可以被轉移走,從而避免copy,常用於copy構造函數和assign函數:

例如:


#include<iostream>
#include<string.h>

class Vec
{
public:
    Vec() 
    {
        p_ = nullptr;
        sz_ = 0;
    }

    ~Vec()
    {
        delete [] p_;
    }

    Vec(int sz)
    {
        p_ = new char[sz];
        sz_ = sz;
    }

    Vec(const Vec &other)
    {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        // 拷貝內容
        p_ = new char[other.sz_];
        sz_ = other.sz_;
        memcpy(p_, other.p_, sz_);
    }

    Vec(Vec &&other) // 使用右值參數
    {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        // 將內容轉移走
        p_ = other.p_;
        sz_ = other.sz_;
        other.p_ = nullptr;
        other.sz_ = 0;
    }

    Vec& operator = (const Vec &other)
    {
        std::cout << __PRETTY_FUNCTION__ << '\n';
        // 拷貝內容
        p_ = new char[other.sz_];
        sz_ = other.sz_;
        memcpy(p_, other.p_, sz_);
        return *this;
    }

    Vec& operator =(Vec && other) //使用右值參數
    {
        //std::cout << "Vec& operator=(Vec &&)\n";
        std::cout << __PRETTY_FUNCTION__ << '\n';
        // 將內容轉移走
        p_ = other.p_;
        sz_ = other.sz_;
        other.p_ = nullptr;
        other.sz_ = 0;
        return *this;
    } 

    char & operator[](int idx)
    {
        return p_[idx];
    }

    int size() const
    {
        return sz_;
    }

    void print() {
        for (auto i = 0; i < sz_; ++i)
            std::cout << p_[i];
        std::cout << '\n';
    }
private:
    char *p_;
    int sz_;
};

Vec getVec(int sz)
{
    Vec v, v2;

    if (sz > 0) {
    for (auto i = 0; i < v.size(); ++i)
        v[i] = 'a'+ 1; 
        return v;
    } else {
    return v2;
    }
}

int main()
{
    Vec v1 {getVec(10)};
    Vec v2;
    v2 = getVec(10);
}

運行上述程序會打印:

c++rvalue.png

我們可以看到main函數中v1被構造時使用了右值構造函數Vec(Vec&&), 結果就是將內容做了move,而不是copy,從而提高了效率。 我在getVec中加入判斷sz>0這個怪異的代碼是故意刁難編譯器,讓它無法做返回值優化(RV0),否則你連這個copy構造調用都看不到,都被編譯器優化掉了.因為從getVec()返回的值在複製給v1後就消失了,所以返回值的內容被move的結果是合理的,既然它將消失,我直接就偷了它的內容.注意平時做人可不能這樣,這涉及人品問題,還是先溝通打個招唿為好.

而main函數中對v2的賦值就更加明顯了,先是v2使用缺省構造函數,完成構造,然後是通過
Vec& Vec::operator=(Vec&&) move assign把返回值的內容move進v2裏麵,原因與上麵相同.

從上麵的例子可以看出,C++11的右值支持確實可以提高效率,當把class做成可以move時,就不怕函數直接返回超大對象. 以前在舊的C++代碼中,我們必須給函數傳入一個指針或者引用作為接收內容的緩衝區,在C++11後,這種現象會減少,運算表達式看起來會更加自然.

對稱性

對稱性是自然的一種屬性. 如果你觀察上麵的代碼,你會發現右值隻在表達式的右邊或者作為函數調用的參數存在,這看起來幾乎完全屬於被動. 別忘記, 右值是對象時,你可以調用它的成員函數!

例如:
getVec(10).print();

那麼既然時臨時對象rvalue即將消失,如果我調用它的函數時也許可以做些不同事情?

C++11以前沒有rvalue概念時,我們寫成員函數,對this指針的修飾隻有三種,例如下麵
成員函數準對三種不同的對象指針this的成員函數f():


struct Test {
    void f() {
        std::cout << "normal this" << std::endl;
    }
    void f() const {
        std::cout << "normal const this" << std::endl;
    }
    void f() volatile {
        std::cout << "volatile this" << std::endl;
    }

分別對應 Test, const Test, volatile Test, 沒左、右值之分。在c++11上在,這三個f()既可以使用在左值又可以使用在右值上。

C++11之後,我們有了左、右值之分,我的成員函數如何知道我是在左值對象上被調用還是右值對象上被調用?

方法是寫的更加詳細:


#include <iostream>

struct Test {
#if 0
    void f() {
        std::cout << "normal this" << std::endl;
    }
    void f() const {
        std::cout << "normal const this" << std::endl;
    }
    void f() volatile {
        std::cout << "volatile this" << std::endl;
    }
#else
    void f() & {  //在左值上被調用
        std::cout << "lvalue this" << std::endl;
    }

    void f() && //在右值上被調用
    {
        std::cout << "rvalue this" << std::endl;
    }

    void f() const & { //在const左值上被調用
        std::cout << "const lvalue this" << std::endl;
    }

    void f() const && { //在const右值上被調用
        std::cout << "const rvalue this" << std::endl;
    }
#endif

#if 0
    void f() volatile & {
        std::cout << "volatile lvalue this" << std::endl;;
    }
    void f() volatile && {
        std::cout << "volatile rvalue this" << std::endl;
    }
#endif
};

static Test t;
Test &l()
{
    return t;
}

const Test &l_const()
{
    return t;
}

Test r()
{
    return t;
}

const Test r_const()
{
    return t;
}

int main()
{
    l().f();
    l_const().f();
    r().f();
    r_const().f();
}

運行上述程序的結果:

c++rvalue2.png

可以看到編譯器選擇了分別對應於左值和右值的f()函數調用,到了這裏,你可以給Vec類增加一個print2()函數,當是右值時,把內容傳給一個對象,或者發起一個網絡連接等瘋狂的想法。

void Vec::print2() const && {
    auto s = Socket::connet("www.rvalue.com", 80);
    s.send("rvalue");
}

上麵有些f函數注釋掉了,你可以把它打開看看編譯結果,編譯器會報錯,是關於函數重載和二義性,可以加深理解。

所有這些都是為了對稱性,也可以說是完整性,一個右值可以出現在看起來被動的地方,也可以在成員函數主動被調用時被識別。


閱讀type_traits頭文件碰到的
--------------------------------------
我在閱讀type_traits時碰到的is_function模板,其中有些形式如果沒有上述概念,會一頭霧水,現在應該容易理解了,不要被它的代碼嚇到,模板看起來怪異,除非特別難的,否則還是能理解的:

``C++

 template<typename>
    struct is_function;

/// is_function
  template<typename>
    struct is_function
    : public false_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...)>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) &>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) &&>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......)>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) &>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) &&>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) const>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) const &>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) const &&>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) const>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) const &>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) const &&>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) volatile>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) volatile &>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) volatile &&>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) volatile>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) volatile &>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) volatile &&>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) const volatile>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) const volatile &>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes...) const volatile &&>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) const volatile>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) const volatile &>
    : public true_type { };

  template<typename _Res, typename... _ArgTypes>
    struct is_function<_Res(_ArgTypes......) const volatile &&>
    : public true_type { };


is_function模板是探測一種類型是否存在某種調用方式,它的基類true_type裏麵有一個靜態成員value,true_type是設置成true的,而false_type是這隻成false的,它們實際上是integral_constant的typedef :
https://en.cppreference.com/w/cpp/types/integral_constant
這是C++模板常用的技術。

例如我寫一些例子代碼來探測是否某個函數對象存在某種方式的調用,我分別對普通的this調用和const this做了判斷。


template<typename T, typename ... ArgTypes>
struct use_is_function {
    void test(T &t, ArgTypes... args) {
        int v;
        if ((v = is_function<T(ArgTypes...)>::value)) {
            t(args...);
            std::cout << "is function :" << v << std::endl;
        }
    }
    void test(const T &t, ArgTypes... args) {
        int v;
        if ((v = is_function<T(ArgTypes...) const &>::value)) {
            t(args...);
            std::cout << "is function :" << v << std::endl;
        }
    }
};

struct Test
{
    int operator()(int) const
    {
        return 1;
    }
};

int main()
{
    Test t;
    use_is_function<Test, int> u;

    u.test(t, 1); // 判斷是否存在 operator()(int)
    u.test((const Test &)t, 1); //判斷是否存在 operator() (int) const
    return 0;
}

總結

C++11的右值雖然增加了學習難度,入門門檻更高,但是對於返回大對象提高運行效率和表達式變的看起來更加自然,兩者之間做到了很好的結合。

最後更新:2017-08-31 10:33:19

  上一篇:go  2015年的智能大招是怎麼憋出來的?
  下一篇:go  啟發:從MNS事務消息談分布式事務