欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++11:右值引用、移动构造、std::move, 以及使用emplace_back代替push_back

程序员文章站 2022-03-22 21:37:46
...

    最近在写一段代码的时候,突然很好奇C++11中对push_back有没有什么改进以增加效率,上网搜了一些资料,发现果然新增了emplace_back方法,比push_back的效率要高很多。

1、右值引用
C++11引入了右值引用,用&&表示右值引用,如int &&a = add(1,2)。

先了解下什么是左值和右值,简单的说,下面的表达式:

int a = 10;

等号“=”左边的a为左值,右边的10为右值;

当然这只是表面的定义,进一步说,左值是有固定的内存地址,&a即左值的地址,我们可以把&a保存起来,后续通过&a这个地址读取、修改a的内容;

而右值是一种临时的值,我们很难获取到右值的地址,如上面的10,10的地址在哪里呢,通过常规的代码是很难获取到10的内存地址的,或者即使获取到右值的地址,该地址可能很快失效了,不能后续使用了。

几种常见的右值如下:

硬编码的字面量,如int a =10中的10,char *s = "hello world"中的“hello world”都是右值

函数的返回值,如add(1,2)的返回值3也是右值,这里的3是临时值

表达式的计算值,如int a =1,b =2;int c = a+b;中的a+b的值也是右值,这里a+b的值也是临时值

C++11引入右值引用后,左值和右值又细分为 prvalue, xvalue, lvalue等,有兴趣的可以阅读 http://en.cppreference.com/w/cpp/language/value_category

还是没搞清楚右值是什么东东?没关系!这并不影响继续阅读下文的内容,你只需要知道&&是右值就行啦。

2、移动构造函数:转移类成员的所有权
C++11之前已经有复制构造函数了,相比复制构造函数,移动构造函数不是复制,而是直接转移类成员的所有权

C++11引入了右值引用后,水道渠成的引入了移动构造函数,其参数类型为右值引用,看下面的例子:

class A{

public:



A(A &&o){

  cout<<"move constructor"<<endl;

}



A(const A& o) {

  cout<<"copy constructor"<<endl;

};

移动构造函数和复制构造函数比较像,它把复制构造函数的&替换为&&了,且没有了const。

移动构造函数有什么用呢,我们顾名思义一下,它是用来移动的,移动什么内容呢,移动的是类内部成员/元素的所有权,看下面的例子:

class A{

public:



A(int size) {

  cout<<"constructor"<<endl;

  this->size = size;

  if(size)data = new int[size];

  for (int i = 0; i < size; ++i)data[i] = i;

}



A(const A& o) {

  cout<<"copy constructor"<<endl;

  this->size=o.size;

  data = new int[size];

  memcpy(data,o.data,size*sizeof(int));

}



A(A &&o) {

  cout<<"move constructor"<<endl;

  data=o.data;

  this->size=o.size;

  o.data=nullptr;

  o.size=0;

}



~A(){delete []data;}



private:

  int *data = nullptr;

  int size = 0;

};

在上面的复制构造函数中,我们把o.data的内容复制给this->data,这就是大家常说的深拷贝;

而在上面的在移动构造函数中,我们没有复制o.data的元素,而只是把o.data的指针赋值给this->data,这就是大家常用的浅拷贝,但是与浅拷贝不同的是,移动构造函数还把o.data指针置为空,这样执行移动构造函数后,this->data获得了元素的所有权,而o.data不再拥有之前元素的所有权,o.data元素的所有权被移动/过继给this了,这就是移动构造函数的含义。

明白了什么是移动构造函数,那么问题来了,如何才能调用移动构造函数呢?

当我们执行如下的代码

A a(10);

A b = a;

//输出结果为:

constructor

copy constructor

此时调用的是复制构造函数,因为上述代码是要把a的内容复制给b;而移动构造函数的参数类型是右值引用,要想调用移动构造函数,我们需要传入一个右值引用。前面提到,函数的返回值是临时值,它是右值引用,所以我们编写一个函数,让该函数的返回类型为A,代码如下:

A CreateA(int size) {

  A a(size);

  return a;

}



b = CreateA(10);

//输出结果为:

constructor

move constructor

顺利调用了移动构造函数!为了更方便的调用移动构造函数,C++11还引入了一个特别有用的函数,即std::move。

3、std::move:把一个左值引用“强制转换”为右值引用。
move函数的声明如下:

template< class T >

typename std::remove_reference<T>::type&& move( T&& t ) noexcept;

从函数声明上看move的返回值,返回值是右值引用,所以通过move,我们可以把左值转换为右值引用。

我们使用move修改上面的代码

A a(10);

A b = std::move(a);

//输出结果为:

constructor

move constructor

顺利调用了移动构造函数,而且,是不是更简单,更方便了?

4、具有复制功能的移动构造函数
有童鞋问了,上面代码中的移动构造函数的移动功能,完全取决与代码的实现,假如把移动构造函数编码为复制功能,所谓的移动构造函数还算哪门子移动啊?代码如下

class A {

public:

   A(A &&o) {

     cout << "moving constructor but execute copyping" << endl;

     this->size = o.size;

     data = new int[size];

     memcpy(data, o.data, size * sizeof(int));

   }

}

执行如下的代码

A a(10);

A b = std::move(a);

//输出结果为:

constructor

moving constructor but execute copyping

对,你没看错,在移动构造函数不执行移动,而执行复制是合法,因为移动构造函数执行移动功能只是约定,而不是强制要求,你完全可以在移动构造函数中执行复制功能!

但是,最好不要这么干(移动构造函数中执行复制功能,简直就是给自己挖坑!),而是遵守通用的约定,比如标准库STL中,移动构造函数都是按照约定,实现为移动功能,看下面的例子:

vector<int> v1={1,2,3,4,5};

vector<int> v2=move(v1);

cout<<"v1.size:"<<v1.size()<<endl;

cout<<"v2.size:"<<v2.size()<<endl;

//执行结果为:

v1.size:0

v2.size:5

5、“移动式”插入元素emplace_back/emplace:更高效的向STL容器插入元素
先看下vector::push_back插入元素的过程

vector<A> vec;

A a(10);

vec.push_back(a);

//输出结果为:

constructor

copy constructor

从输出结果看,push_back过程中除了调用一次构造函数,还额外调用了一次复制构造函数,额外调用复制构造函数甚是浪费时间,假如是A a(10000000),复制起来可是非常耗时的!如何避免额外的复制呢?

emplace_back来帮助你,其函数声明如下:

template< class... Args >

void emplace_back( Args&&... args );

从函数声明中,我们看到其参数是右值引用,右值引用可以用来干什么?移动元素的所有权!让我们执行下面的代码:

vector<A> vec;

A a(10);

vec.emplace_back(std::move(a));

//输出结果为:

constructor

move constructor

从输出结果看,此时调用了一次构造函数,和一次移动构造函数,而移动构造函数基本是不耗时的。很明显,使用emplace_back比push_back效率更高。

另外,C++11中,上面的代码可以简化为

vector<A> vec;

vec.emplace_back(10);

//输出结果为:

constructor

从输出结果看,此时只调用了一次构造函数,连移动构造函数都省掉了,这是因为emplace_back把参数10完美转发给A的构造函数,直接构造了一个元素,而这个元素是直接存放在vector容器中的,为了节省一点执行时间,C++11也是拼了。

简单总结以下,push_back是“复制式”(即调用复制构造函数)的插入元素,而emplace_back是“移动式”(即调用移动构造函数)的插入元素;

同理,vector::insert、set::insert、map::insert是复制式的插入元素,他们的移动式插入函数是emplace

两个注意事项:

调用emplace_back时需要注意,不要把vec.emplace_back(std::move(a)),错误的写成vec.emplace_back(a),看下面的例子:

vector<A> vec;

A a(10);

vec.emplace_back(a);

//输出的结果为:

constructor

copy constructor

从输出结果看,此时调用的是复制构造函数而不是移动构造函数,因为传入的参数a不是右值引用,需要先调用a的复制构造函数生成一个副本,然后把副本的右值引用传递给emplace_back,最终造成vec.emplace_back(a)等效与vec.push_back(a)。

当自定义类A没有移动构造函数时,vec.emplace_back(std::move(a))也等效与vec.push_back(a)。

2.1、说明
       C++11中,针对顺序容器(如vector、deque、list),新标准引入了三个新成员:emplace_front、emplace和emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。

       当调用push或insert成员函数时,我们将元素类型的对象传递给它们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。

       emplace函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配。emplace函数在容器中直接构造元素。传递给emplace函数的参数必须与元素类型的构造函数相匹配。

       其它容器中,

std::forward_list中的emplace_after、emplace_front函数,

std::map/std::multimap中的emplace、emplace_hint函数,

std::set/std::multiset中的emplace、emplace_hint,

std::stack中的emplace函数,

       等emplace相似函数操作也均是构造而不是拷贝元素。

       emplace相关函以数可减少内存拷贝和移动。当插入rvalue,它节约了一次move构造,当插入lvalue,它节约了一次copy构造。

例子1
假设我有一个结构:

struct my_pair {
    int foo, bar;
};
我想有效地添加一堆这些,而不是创建一个临时的,然后丢弃它:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct my_pair {
    int foo, bar;
};
int main()
{
    vector<my_pair> v;
    v.push_back(41, 42);                          // [a] error: no matching function for call
    v.push_back({41,42});                       // [b] works
    v.emplace_back(41,42);                      // [c] candidate: 'my_pair::my_pair()'
    v.emplace_back({41,42});                   // [d] error: no matching function for call
    v.emplace_back(my_pair{41,42});   // [e] works
}

现在,如果我将构造函数和复制构造函数添加到我的代码中:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct my_pair {
    int foo, bar;
    my_pair(int foo_, int bar_) : foo(foo_), bar(bar_) 
    {  cout << "in cstor ->"; }
    my_pair(const my_pair& copy) : foo(copy.foo), bar(copy.bar)
    {  cout << "in copy cstor ->";}
};
int main()
{
    vector<my_pair> v;
    v.push_back(41, 42);                        // [a] error: no matching function for call
    v.push_back({41,42});                      // [b] in cstor ->in copy cstor ->
    v.emplace_back(41,42);                 // [c] in cstor ->   #如果有一个构造函数将参数映射到成员,似乎这是最有效的代码 
    v.emplace_back({41,42});              // [d] error: no matching function for call
    v.emplace_back(my_pair{41,42});   // [e] in cstor ->in copy cstor ->
}

如果我添加一个移动构造函数: 

#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct my_pair {
    int foo, bar;
    my_pair(int foo_, int bar_) : foo(foo_), bar(bar_) 
    {   cout << "in cstor ->";   }
    my_pair(const my_pair& copy) : foo(copy.foo), bar(copy.bar)
    {   cout << "in copy cstor ->";  }
    my_pair(my_pair&& move_) : foo(move_.foo), bar(move_.bar)
    {   cout << "in move cstor->";  }
};
int main()
{
    vector<my_pair> v;
    v.push_back(41, 42);                       // [a] error: no matching function for call
    v.push_back({41,42});                      // [b] in cstor ->in move cstor->
    v.emplace_back(41,42);                  // [c] in cstor ->
    v.emplace_back({41,42});                // [d] error: no matching function for call  任然报错
    v.emplace_back(my_pair{41,42});   // [e] in cstor ->in move cstor->
}

居然替换了拷贝构造函数,调用了Move构造函数。似乎添加了一个移动构造函数 push_back(T&&) 正在被调用,这导致push_back与emplace_back相同的性能。

问题:  

我读到MSVC没有为您添加移动构造函数: 为什么在调用std :: vector :: emplace_back()时调用复制构造函数?

如果我知道有一个构造函数将每个成员作为参数,我只能获得emplace的全部好处吗?

我应该坚持下去 push_back?有没有理由使用 emplace_back(structname{1,2,3}) 代替 push_back({1,2,3}) 因为它最终会召唤 push_back(T&&) ?

第三,怎么做 emplace_back(arg1,arg2,etc),它的魔力完全避免复制或移动构造函数?

为什么push_back(T &&)在没有添加结构名称的情况下工作?

这是因为 push_back签名。 push_back()没有推断出这个论点,这意味着这样做 push_back({1, 2}),首先创建一个具有向量元素类型类型的临时对象并使用初始化 {1, 2}。那个临时对象将是传递给它的那个 push_back(T&&)。

我应该坚持使用push_back吗?是否有任何理由使用emplace_back(structname {1,2,3})而不是push_back({1,2,3}),因为它最终会调用push_back(T &&),并且更容易键入?

基本上, emplace* 函数化和消用于优除创建临时对象以及在插入对象时复制或移动构造对象的成本。但是,对于聚合数据类型的情况,执行类似的操作 emplace_back(1, 2, 3) 是不可能的,你可以插入它们的唯一方法是创建一个临时的,然后复制或移动,然后通过所有方式更喜欢更精简的语法,并去 push_back({1,2,3}),它基本上具有相同的性能 emplace_back(structname{1,2,3})。

http://landcareweb.com/questions/30988/dai-you-structde-c-11-emplace-backhe-push-backyu-fa

例子2
首先,写了一个类用于计时,

//time_interval.h
#pragma once
 
#include <iostream>
#include <memory>
#include <string>
#ifdef GCC
#include <sys/time.h>
#else
#include <ctime>
#endif // GCC
 
class TimeInterval
{
public:
    TimeInterval(const std::string& d) : detail(d)
    {
        init();
    }
 
    TimeInterval()
    {
        init();
    }
 
    ~TimeInterval()
    {
#ifdef GCC
        gettimeofday(&end, NULL);
        std::cout << detail 
            << 1000 * (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1000 
            << " ms" << endl;
#else
        end = clock();
        std::cout << detail 
            << (double)(end - start) << " ms" << std::endl;
#endif // GCC
    }
 
protected:
    void init() {
#ifdef GCC
        gettimeofday(&start, NULL);
#else
        start = clock();
#endif // GCC
    }
private:
    std::string detail;
#ifdef GCC
    timeval start, end;
#else
    clock_t start, end;
#endif // GCC
};
 
#define TIME_INTERVAL_SCOPE(d)   std::shared_ptr<TimeInterval> time_interval_scope_begin = std::make_shared<TimeInterval>(d)


使用方法就是在作用域中使用宏TIME_INTERVAL_SCOPE(d),d为打印用的字符串,输出作用域的耗时情况。

其次,看一下现在push到vector的5种方法的耗时情况对比:

#include <vector>
#include <string>
#include "time_interval.h"
 
int main() {
    std::vector<std::string> v;
    int count = 10000000;
    v.reserve(count);       //预分配十万大小,排除掉分配内存的时间
 
    {
        TIME_INTERVAL_SCOPE("push_back string:");
        for (int i = 0; i < count; i++)
        {
            std::string temp("ceshi");
            v.push_back(temp);// push_back(const string&),参数是左值引用
        }
    }
 
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back move(string):");
        for (int i = 0; i < count; i++)
        {
            std::string temp("ceshi");
            v.push_back(std::move(temp));// push_back(string &&), 参数是右值引用
        }
    }
 
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back(string):");
        for (int i = 0; i < count; i++)
        {
            v.push_back(std::string("ceshi"));// push_back(string &&), 参数是右值引用
        }
    }
 
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back(c string):");
        for (int i = 0; i < count; i++)
        {
            v.push_back("ceshi");// push_back(string &&), 参数是右值引用
        }
    }
 
    v.clear();
    {
        TIME_INTERVAL_SCOPE("emplace_back(c string):");
        for (int i = 0; i < count; i++)
        {
            v.emplace_back("ceshi");// 只有一次构造函数,不调用拷贝构造函数,速度最快
        }
    }
}


vs2015 release下编译,运行结果:

push_back string:327 ms 
push_back move(string):213 ms 
push_back(string):229 ms 
push_back(c string):215 ms 
emplace_back(c string):122 ms

第1中方法耗时最长,原因显而易见,将调用左值引用的push_back,且将会调用一次string的拷贝构造函数,比较耗时,这里的string还算很短的,如果很长的话,差异会更大

第2、3、4中方法耗时基本一样,参数为右值,将调用右值引用的push_back,故调用string的移动构造函数,移动构造函数耗时比拷贝构造函数少,因为不需要重新分配内存空间。

第5中方法耗时最少,因为emplace_back只调用构造函数,没有移动构造函数,也没有拷贝构造函数。

为了证实上述论断,我们自定义一个类,并在普通构造函数、拷贝构造函数、移动构造函数中打印相应描述:

#include <vector>
#include <string>
#include "time_interval.h"
 
class Foo {
public:
    Foo(std::string str) : name(str) {
        std::cout << "constructor" << std::endl;
    }
    Foo(const Foo& f) : name(f.name) {
        std::cout << "copy constructor" << std::endl;
    }
    Foo(Foo&& f) : name(std::move(f.name)){
        std::cout << "move constructor" << std::endl;
    }
 
private:
    std::string name;
};
int main() {
    std::vector<Foo> v;
    int count = 10000000;
    v.reserve(count);       //预分配十万大小,排除掉分配内存的时间
 
    {
        TIME_INTERVAL_SCOPE("push_back T:");
        Foo temp("ceshi");
        v.push_back(temp);// push_back(const T&),参数是左值引用
        //打印结果:
        //constructor
        //copy constructor
    }
 
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back move(T):");
        Foo temp("ceshi");
        v.push_back(std::move(temp));// push_back(T &&), 参数是右值引用
        //打印结果:
        //constructor
        //move constructor
    }
 
    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back(T&&):");
        v.push_back(Foo("ceshi"));// push_back(T &&), 参数是右值引用
        //打印结果:
        //constructor
        //move constructor
    }
 
    v.clear();
    {
        std::string temp = "ceshi";
        TIME_INTERVAL_SCOPE("push_back(string):");
        v.push_back(temp);// push_back(T &&), 参数是右值引用
        //打印结果:
        //constructor
        //move constructor
    }
 
    v.clear();
    {
        std::string temp = "ceshi";
        TIME_INTERVAL_SCOPE("emplace_back(string):");
        v.emplace_back(temp);// 只有一次构造函数,不调用拷贝构造函数,速度最快
        //打印结果:
        //constructor
    }
}

结论:
        emplace/emplace_back的性能比insert和push_back的性能要提高很多,我们应该尽量用emplace/emplace_back来代替原来的插入元素的接口以提高性能。需要注意的是,我们还不能完全用emplace_back来取代push_back等老接口,因为在某些场景下并不能直接使用emplace来进行就地构造,比如,当结构体中没有提供相应的构造函数时就不能用emplace了,这时就只能用push_back。

        在C++11情况下,类中有对应参数的构造函数时,果断用emplace_back代替push_back。
 

上一篇: C++右值引用

下一篇: 右值引用