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

前言


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

本文的主要关注内容为: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::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
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast::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 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
        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
    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
    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)...);
    }

参考文章


  • A Brief Introduction to Rvalue References,强力推荐,C++之父和标准委员会参与的文章
  • Translation by sib9,推荐,翻译
  • Cpp Reference Move Constructor,推荐,CppReference官方解释
  • IBM C++11 标准新特性: 右值引用与转移语义,推荐,写得不错
  • MSDN move constructor/assginment,MSDN作品
  • Another Blog 0 - By Thomas Becker,Google搜到的
  • Another Blog 1 - By Alex Allain,Google搜到的
  • Another Blog 2 - By Danny Kalev,Google搜到的
  • Another Blog3 - By K Hong,Google搜到的

你可能感兴趣的:(特性速览 - C++11右值引用/转移语义/完美转发)