特性速览 - C++11右值引用/转移语义/完美转发

前言


本文主要根据C++标准委员会写的 A Brief Introduction to Rvalue ReferencesIBM的博客 ,加上自己个人的理解完成。有瑕疵的地方或明显的问题的话,请在下面评论栏评论,谢谢。

本文的主要关注内容为:L-value/R-value,Move语义和其存在价值(通过my_string的小例子来讲述),完美转发的使用。

右值引用/转移语义/完美转发是一个小的改进,但对库设计者(比如STL)来说帮助很大。通过编译器语法层面支持,使得库开发者(Boost的作者们)不需要用奇淫巧技(模板元编程/预处理元编程)来获得转移语义,并且完美转发使得可变参数模板(varidic template)支持得以实现。下面是varadic argument链接地址

引用一下IBM博客上面的话。

右值引用,表面上看只是增加了一个引用符号,但它对 C++ 软件设计和类库的设计有非常大的影响。它既能简化代码,又能提高程序运行效率。每一个 C++ 软件设计师和程序员都应该理解并能够应用它。我们在设计类的时候如果有动态申请的资源,也应该设计转移构造函数和转移拷贝函数。在设计类库时,还应该考虑 std::move 的使用场景并积极使用它。

左值/右值引用(Lvalue/Rvalue Reference)


An rvalue reference behaves just like an lvalue reference except that it can bind to a temporary (an rvalue), whereas you can not bind a (non const) lvalue reference to an rvalue.

  • Lvaue-Reference
A a;
A& a_ref1 = a;  // an lvalue reference
  • Rvalue-Reference
A a;
A&& a_ref2 = a;  // an rvalue reference
  • Rvalue-Bind-Tempory
A&  a_ref3 = A();  // Error!
A&& a_ref4 = A();  // Ok

右值引用引入的用途:支持转移(move)语义和完美转发(perfect forwarding)。有了move语义和perfect forwarding之后,库设计者比如说STL设计者就可以设计出更直观易于使用,并且高效的模板库来。

转移(Move)语义


  • STL对move的实现,基于之前讲的右值引用,并从一定程度上利用了模板元编程,static_cast<typename std::remove_reference<_Tp>::type调用模板元函数计算出类型,然后加上&&组成右值引用。想了解模板元编程基础概念的朋友,可以参考我的模板元编程笔记。详细代码如下所示:
  /**
   *  @brief  Convert a value to an rvalue.
   *  @param  __t  A thing of arbitrary type.
   *  @return The parameter cast to an rvalue-reference to allow moving it.
  */
  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
  • move语义的价值,有些时候,我们往往想转移资源(比如可以是glibc.so里面实现的进程heap,也就是malloc得来的虚拟内存)的所有权,而不是拷贝资源(需要重新malloc,再进行memcpy),这个时候我们想要的是一个move语义直接转移以下资源的所有权(ownership)。move语义可以从编译器语法层面上支持我们做这件事情。

my_string容器(用来说明move语义的价值)


在这里为了说明move语义对于库开发者的价值,我定义了一个非常简化的my_string,只包含两个成员变量,指向c_string的data_,和表示长度的len_。这个string主要负责管理资源,也就是说让这个容器来帮助我们在进程的heap上分配内存和释放内存,分别调用malloc和free。

值得注意的是,代码中alloc_and_construct是一个Expensive的操作,下一节的测试程序会以这个操作个数为评判标准,在标准输出中会看到“"Expensive Operation”。在Move语义的支持下,该输出会少,也就是说零动态开销的move语义提高了程序的性能。详细的代码如下所示:

class my_string {
private:
    char *data_{nullptr};
    size_t len_{0};

    void alloc_and_construct(const char *s) {
        cout << "Expensive Operation" << endl;
        data_ = new char[len_ + 1];
        memcpy(data_, s, len_);
        data_[len_] = '\0';
    }

    void go_deallocate(char *data) {
        if (data_)
            free(data);
    }

public:
    my_string() = default;

    virtual ~my_string() {
        cout << "Destructor" << endl;
        go_deallocate(data_);
    }

    my_string(const char *p) : len_(strlen(p)) {
        alloc_and_construct(p);
    }
};

测试程序


测试程序主要要说明拷贝构造函数和拷贝赋值函数的调用在有些时候是不必要的,比如说:1)在我创建匿名类对象的时候,我不希望执行拷贝语义,我只想转移指针的所有权;2)我希望直接转移指针所有权从object_a到object_b,因为object_a之后将不再使用了。
测试程序如下:

int main() {
    //Assignment, transfer the ownership from temporary to a
    my_string a;
    a = my_string("Hello");

    //Constructor0, transfer the ownership from a to b
    my_string b = move(a);

    //Constructor1, transfer the ownership from temporary to the specific my_string object in vec
    vector<my_string> vec;
    vec.push_back(my_string("World"));
}
  • 简单提一下,vector的push_back方法, 其中参数类型的声明就使用了右值引用,调用的emplace_back使用了下面会讲到的完美转发和可变参数模板,其代码摘要如下:
#if __cplusplus >= 201103L
      void
      push_back(value_type&& __x)
      { emplace_back(std::move(__x)); }

      template<typename... _Args>
        void
        emplace_back(_Args&&... __args);
#endif

拷贝构造/拷贝赋值函数(copy contructor/copy assignment)


针对my_string容器,下面给出了其拷贝构造函数和拷贝赋值函数的定义:

  my_string(const my_string &str) {
        go_deallocate(data_);
        len_ = str.len_;
        alloc_and_construct(str.data_);
        cout << "Copy Constructor is called! source: " << str.data_ << endl;
    }
    
    my_string &operator=(const my_string &str) {
        go_deallocate(data_);
        if (this != &str) {
            len_ = str.len_;
            alloc_and_construct(str.data_);
        }
        cout << "Copy Assignment is called! source: " << str.data_ << endl;
        return *this;
    }

转移构造/转移赋值函数(move constructor/move assignment)


针对my_string容器,下面给出了其拷贝转移函数和转移赋值函数的定义:

    my_string(my_string &&str) {
        go_deallocate(data_);
        cout << "Move Constructor is called! source: " << str.data_ << endl;
        len_ = str.len_;
        data_ = str.data_;
        str.len_ = 0;
        str.data_ = nullptr;
    }

    my_string &operator=(my_string &&str) {
        go_deallocate(data_);
        cout << "Move Assignment is called! source: " << str.data_ << endl;
        if (this != &str) {
            len_ = str.len_;
            data_ = str.data_;
            str.len_ = 0;
            str.data_ = nullptr;
        }
        return *this;
    }

运行分析


  • 在没有定义转移构造函数和转移赋值函数的时候,测试程序的运行结果如下:
Expensive Operation
Expensive Operation
Copy Assignment is called! source: Hello
Destructor
Expensive Operation
Copy Constructor is called! source: Hello
Expensive Operation
Expensive Operation
Copy Constructor is called! source: World
Destructor
Destructor
Destructor
Destructor
  • 在定义了转移构造函数和转移赋值函数的时候,测试程序的运行结果如下:
Expensive Operation
Move Assignment is called! source: Hello
Destructor
Move Constructor is called! source: Hello
Expensive Operation
Move Constructor is called! source: World
Destructor
Destructor
Destructor
Destructor
  • 可以观察发现转移构造函数和转移赋值函数大大减少了不不要的拷贝,使得Expensive Operation得到了减少。这就是Move语义的魅力所在,在编译器语法分析的阶段支持了这种特性,不用用元编程的奇淫巧技,并且也不用由动态开销。

  • 完整的程序,可以参看 my_string Github链接,读者可以通过注释掉#define ENABLE_MOVE进行只有copy constructor/assginment的测试。编译的话,请在你的Terminal输入g++ -std=c++11 MoveSyntaxDemo.cpp -o your_exe_name

STL中的注意点


STL中有些类型的copy assignment/constructor 声明为delete,因为对象里面的状态或资源比较多,并且拷贝没有实际价值。

Some types are not amenable to copy semantics but can still be made movable. For example:

  • fstream
  • unique_ptr (non-shared, non-copyable ownership)
  • A type representing a thread of execution

完美转发(Perfect Forwarding)


  • STL对Perfect Forwarding的实现,代码如下:
  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
            " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }
  • STL-shared_ptr例子, STL中的shared_ptr利用了可变参数模板和完美转发,例子如下:
  /**
   *  @brief  Create an object that is owned by a shared_ptr.
   *  @param  __args  Arguments for the @a _Tp object's constructor.
   *  @return A shared_ptr that owns the newly created object.
   *  @throw  std::bad_alloc, or an exception thrown from the
   *          constructor of @a _Tp.
   */
  template<typename _Tp, typename... _Args>
    inline shared_ptr<_Tp>
    make_shared(_Args&&... __args)
    {
      typedef typename std::remove_const<_Tp>::type _Tp_nc;
      return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
                       std::forward<_Args>(__args)...);
    }

参考文章


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容