c++11 几个容易混淆的概念

右值引用

C++11 引入了右值引用的概念,使用&&表示。
首先简单介绍右值的概念,简单的讲,凡是真正存在内存当中,而不是寄存器中的值是左值,其余都是右值。更通俗的说法取地址操作可以得到的都是左值,其余都是右值。例如

int a = 2;

a 中就是一个左值,相对的,2 就是一个右值。关于右值更详细严谨的介绍见https://en.cppreference.com/w/cpp/language/value_category

移动构造函数

在c++11 之前,类包括构造函数,析构函数,拷贝构造函数,赋值构造函数。对于存在指针变量的类来讲,其拷贝构造函数,赋值构造函数必须实现指针变量的深拷贝,这可能会涉及到比较耗时的操作(比如string 类存储了一个超长字符串,在调用其拷贝构造或赋值构造时需要超长字符串的拷贝)。
移动构造函数相对拷贝构造函数和赋值构造函数而言不会进行成员变量的深拷贝而是交换其所有权,这样就避免的拷贝时带来的性能损耗。
移动构造的函数声明如下

class_name ( class_name && );

emplace_back

c++11 容器新增了 emplace_back, emplace等方法向容器中加入新的元素。以 vector 的emplace_back 为例,其函数声明如下

template< class... Args >
void emplace_back( Args&&... args );

emplcae_back 接收一个右值引用,调用其移动构造函数,将对象移动到容器中,而之前的push_back 是调用一次对象的拷贝构造函数, 容器中存储的是拷贝后的副本。

std::move

std::move 的作用是将左值转为右值引用类型。

示例

示例代码测试了 移动构造函数,emplace_back, push_back, std::move

/*************************************************************************
    > File Name: emplace.cpp
    > Author: ce39906
    > Mail: ce39906@163.com
    > Created Time: 2018-07-26 15:03:11
 ************************************************************************/
#include <vector>
#include <cstring>
#include <iostream>
class A
{
  public:
    A (const int size) : size(size)
    {
        if (size)
        {
            data = new char[size];
        }
        std::cout << "I'm constructor.\n";
    }

    A (const A& other)
    {
        size = other.size;
        data = new char[size];
        memcpy(data, other.data, size * sizeof(char));
        std::cout << "I'm copy constructor.\n";
    }

    A (A&& other)
    {
        size = other.size;
        data = other.data;

        other.size = 0;
        other.data = nullptr;
        std::cout << "I'm move constructor.\n";
    }

  private:
    int size;
    char* data = nullptr;
};

int main()
{
    std::vector<A> vec;
    vec.reserve(1024);

    A tmp(5);
    std::cout << "push_back a left value.\n";
    vec.push_back(tmp);

    std::cout << "push_back a right value with std::move.\n";
    vec.push_back(std::move(tmp));

    std::cout << "emplace_back a left value.\n";
    vec.emplace_back(tmp);

    std::cout << "emplace_back a right value with std::move.\n";
    vec.emplace_back(std::move(tmp));

    std::cout << "emplace_back in place.\n";
    vec.emplace_back(5);

    std::cout << "=========================================\n";
    std::cout << "test with buildin string move and emplace_back\n";
    std::cout << "=========================================\n";

    std::vector<std::string> str_vec;
    str_vec.reserve(1024);

    std::string str = "I'd like to be inserted to a container";

    std::cout << "before emplace_back to vec, str is:\n";
    std::cout << str << std::endl;
    std::cout << "c_str address is " << (void*) str.c_str() << std::endl;

    str_vec.emplace_back(std::move(str));

    std::cout << "after emplace_back to vec, str is:\n";
    std::cout << str << std::endl;
    std::cout << "c_str address is " << (void*) str.c_str() << std::endl;
    std::cout << "c_str address of the string in container is "
        << (void*) str_vec.front().c_str() << std::endl;
}

编译代码

g++ --std=c++11 emplace.cpp -o emplace

执行,输出结果如下
emplace

从执行结果中,我们可以得出以下结论

  1. push_back 可以接收左值也可以接受右值,接收左值时使用拷贝构造,接收右值时使用移动构造
  2. emplace_back 接收右值时调用类的移动构造
  3. emplace_back 接收左值时,实际上的执行效果是先对传入的参数进行拷贝构造,然后使用拷贝构造后的副本,也就是说,emplace_back在接收一个左值的时候其效果和push_back一致!所以在使用emplace_back 时需要确保传入的参数是一个右值引用,如果不是,请使用std::move()进行转换
  4. emplace_back 接收多个参数时,可以调用匹配的构造函数实现在容器内的原地构造
  5. 使用string 类验证了移动构造函数式对类成员所有权的传递,从上图中看到string 在插入前c_str的地址和使用emplace_back 移动到容器后的c_str的地址一致。并且移动后字符串c_str 的地址指向其他位置。