第八章 微调

四十一 针对可复制的形参,在移动成本低并且一定会被复制的前提下,考虑将其按值传递

当构造函数需要对左值和右值进行分别处理的时候,我们有两种方法,一种是重载接受左值和右值的构造函数,另一种是写万能引用构造函数,这两种方法都有一定的局限性,左值右值重载构造函数会导致代码的冗余,而万能引用会匹配一些错误的类型。

有一种新的方式,就是使用按值传递,函数内部将局部变量强转为右值进行移动构造。代码如下:

class A
{
public:
    void addstr(string s) { //按值传入
        vs.push_back(move(s));
    }
    void printA() {
        for (auto it : vs) cout << it << ' ';
        cout << endl;
    }
private:
    vector<string> vs;
};

函数addstr()在接受左值的时候,使用的是复制操作,而在接受右值的时候,使用的是移动操作,因为string已经针对左值右值实现了相应的构造函数。

现在针对左值右值构造的问题,一共有3个解决方法:

  • 重载:针对左值,传入的变量将会被复制到容器中,针对右值,传入的变量将会被移动到容器中。也就是说,对于左值成本是一次复制,右值成本是一次移动。
  • 万能引用:绑定到万能引用上后,由于内部使用了完美转发,所以对于左值仍然是被复制进容器中,而右值则是移动到容器中。对于左值成本是一次复制,右值成本是一次移动。
  • 按值传递:无论传入的是左值还是右值,形参都会进行一次构造,如果传入左值就是复制构造,传入右值就是移动构造。而在函数内部又进行了一次移动操作,对于左值成本是一次复制加一次构造,右值成本是两次移动。

这三个方法各有优劣:

  • 对于可复制的形参才考虑按值传递,不满足此条件的只移对象不需要复制也不饿能复制,对按值传递来说,必然是使用移动构造来创建该副本。例如unique_ptr,如果类内部包含此对象的话,那么该类不需要复制构造函数,也不可以又复制构造函数,而是仅需要一个移动构造函数。这时候,以右值为形参的构造函数就仅需要一次移动操作,而按值传递的构造函数则需要经历:make_unique构造->移动构造入形参->将形参移动到容器中(通过move()),这需要两次移动操作。

  • 按值传递仅仅是在形参移动成本低廉的情况下,才值得考虑,也就是说如果移动构造成本高昂的话,就尽量要减少移动构造的次数,就像要减少复制构造的次数一样。

  • 如果在传入容器的时候需要进行判断,也就是说只有一部分才能被移动到容器中,另一部分不满足限制条件不能移动到容器中的话,使用按值传递就会多出不必要的操作,原因就在于即使不能被植入到容器中,形参的构造和析构也是有成本的,而这成本是不必要的。如果按引用传递就不会有这些问题。

  • 当函数内部使用复制构造的时候:

    //按值传递版本
    class A{
    public:
      void f(string str){
          text = move(str);
      }
      ...
    private:
      string text;
    };
    //按引用传递版本
    class A{
    public:
      void f(string& str){
          text = str;
      }
      //暂时不考虑右值构造函数
      ...
    private:
      string text;
    };

    上述代码中的移动版本仅需要一次复制或对右值进行一次移动,而对于引用传递版本来说,string类在进行复制赋值的时候,如果text的容量大于str的长度的话,可以直接复制传入,但是如果小于str的长度,就需要进行隐式的内存析构和重新分配,这就会造成较大的成本。

  • 最后一个问题是切片问题,这个问题会发生在按值传递的构造函数中。

四十二 考虑置入而非插入

本节介绍的就是emplace系列函数。

对于容器来说,传入对象的时候通常采用复制操作,假设现在有一个vector\,并有如下操作:

vector<string> v;
v.push_back("abcd");

这里会进行一次隐式构造,由字符串”abcd”出发隐式构造一个string临时变量,并且由于是右值,vector将会把这个值移动传入,最后隐式的string对象将会因为超出作用域而被析构。这里多出了隐式的构造和析构操作。

当使用emplace_back的时候,就能取消掉这个隐式的构造和析构函数。

emplace_back使用了完美转发,所以我们可以通过emplace_back传递任意型别、任意数量和任意组合的实参。并以这些实参出发构造一个容器内存储的对象型别的实参,该实参存储在容器中。

emplace_XXX来源于push_XXX函数,push_XXX的所有后缀版本都对应相应的emplace_XXX版本。

P.s. emplace_hint:插入新元素到容器中尽可能接近于恰在hint前的位置

置入函数拥有更灵活的接口,插入函数接受的是待插入对象,而置入函数接受的则是待插入对象的构造函数实参,这一区别就可以使置入函数得以避免临时对象的创建和析构,但是插入函数就无法避免。

并且置入函数兼容插入函数传入的参数。当使用插入函数的参数形式的时候,置入函数实际上和插入函数做的是同一件事:

string str = "qwer";
v.push_back(str);   
v.emplace_back(str);    //和push_back做的是同一件事

置入比插入高效的情况如下:

  • 欲添加的值是以构造而非赋值方式加入到容器中。当使用构造的时候,置入就会减少隐式的构造析构成本,如果是赋值方式放入容器的时候,置入和插入效果是一致的。

    v.emplace(v.begin(), "abcd"); //构造方式
    v.emplace(v.begin(), str);    //赋值方式
  • 传递的实参型别和容器持有之物的型别不同。当传递的实参型别并非容器持有之物的型别的时候,置入是不需要创建、析构临时对象的。当时当使用和容器持有之物相同的型别的对象的时候,置入并不会比插入要快。

  • 容器不太可能由于出现重复情况而拒绝添加的新值。如果容器不能出现重复元素,而拒绝添加值,这样置入创建的节点就成了多余的操作。

但有时候使用插入函数反而会更加安全:

如果有一个自定义析构器的shared_ptr,那么我们不可以使用make_shared这个函数,而是要使用构造函数传入析构器。

list<shared_ptr<A>> ptrs;
void killA(A *a);
//插入版本
ptrs.push_back(shared_ptr<A>(new A, killA));
ptrs.push_back({new A, killA});
//置入版本
ptrs.emplace_back(new A, killA);

在插入的时候,首先会构造一个shared_ptr,内部有一个指向A的实例的裸指针,然后ptrs会将此shared_ptr插入,如果传入ptrs的时候抛出了异常,那么shared_ptr的对象将会被析构,也可以正确的释放A实例所占用的内存。

但是在置入的时候,如果传入ptrs的时候出现了异常,那么裸指针将会丢失并且不会被析构。这就造成了内存泄露。

我们应尽量让裸指针的创建和shared_ptr的创建之间没有其他的操作。所以这里最好使用以下版本:

//插入版本
shared_ptr<A> spw(new A, killA)
ptrs.push_back(spw);
//置入版本
ptrs.emplace_back(spw);

这样两者都安全了,但是这个时候的置入和插入也没有什么不同了,置入的性能也并不比插入高。

另一种情况就是隐式构造的问题,也就是说有的时候隐式的构造是我们要去避免的,但是通过emplace_XXX函数进行构造,并非是隐式构造,但是却可能导致意想不到的构造结果。

首先是正则表达式类:std::regex。这个类不可以隐式构造:

regex r = nullptr;
regexes.push_back(nullptr);

上述两行代码都是无法通过编译的,因为第1行的代码在赋值的时候,会首先出发nullptr到regex对象的隐式构造,而这个构造是声明了explicit的,2式在插入的时候,首先要创建容器内部类型的对象,这里也设计到隐式构造。

regex r(nullptr);
regexes.emplace_back(nullptr);

上述两行代码都是可以通过编译的,第1行是显式初始化,第2行是emplace_back产生的初始化,这两种初始化都不是隐式初始化,所以可以通过编译。

但是通过编译后,正则表达式的匹配对象如果是空串的话,就会引发很多不必要的bug。

置入函数使用的是直接初始化,所以它们能调用带有explicit声明饰词的构造函数,而插入函数使用的是复制初始化,所以它们不能调用带有explicit声明饰词的构造函数。

综上所述,emplace_back要保证传入的是正确的实参,因为本身是显式要求的构造函数,emplace_back也是满足的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值