三种函数参数类型分析:
// 途径一:针对左值和右值重载 class Widget { public: void addName(const std::string& newName) { names.push_back(newName); } void addName(const std::string&& newName) { names.push_back(std::move(newName)); } private: std::vector<std::string> names; }; // 途径二:使用万能引用 class Widget { public: template<typename T> void addName(T&& newName) { names.push_back(std::forward<T>(newName)); } ... }; // 途径二:按值传递 class Widget { public: void addName(std::string newName) { names.push_back(std::move(newName)); } ... };
重载:无论传入左值还是右值,调用方的实参都会绑定到名字为newName的引用上。而这样做不会再复制或者移动时带来任何成本。再接受左值的重载版本中,被复制入Widget::names;在接受右值的重载版本中,newName被移入Widget::names。成本合计:对于左值是一次复制,对于右值是一次移动。
万能引用:类似于重载,由于使用了std::forward,左值newName被复制入Widget::names;右值newName被移入Widget::names中,成本与上面相同。
按值传递:无论传入的是左值还是右值,针对形参newName都必须实施一次构造。如果传入的是左值,成本是一次复制构造。如果传入的是个右值,成本是一次移动构造。在函数体内,newName需要无条件地移入Widget::names。成本合计:对于左值,是一次复制+一次移动。对于右值,是两次移动。
综合来看,按值传递比按引用传递要多一次额外的移动操作,在移动操作成本较低的时候,对于可复制的形参,可以使用按值传递,避免万能引用带来的一些坏毛病。
构造复制vs赋值复制
std::string str = "hello, what is your name?"; std::string str1(str); std::string str2("hello, My name is Kairri Regishih"); str2 = str;
第4行相比于第2行,第4行可以复用原本申请好的内存,故效率比第2行要高。这就导致经由构造复制形参的成本可能比经由赋值复制形参高出很多。
注意:按值传递在派生类传递给基类时会发生切片问题,所以基类类型特别不适用于按值传递。
emplace_xxx会将对象直接构造在需要的地方,避免了临时变量的构造和析构,从原理上来说,置入函数应该有时比插入函数高效,而不应该有更低效的可能。
优先选用的场景:
待添加的值是以构造而非赋值的方式加入容器,其实赋值只是没有更快,也没有更慢;
传递的实参类型与容器的内置类型不同,需要自动转换生成临时对象;
容器不会由于存在重复值而拒绝待添加的值,否则就会白做一次构造和析构;
置入的缺陷
置入操作有可能带来异常安全性的问题,例如下面代码:
std::list<std::shared_ptr<Widget>> ptrs; void killWidget(Widget* pWidget); ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget)); ptrs.emplace_back(new Widget, killWidget);
如果push_back在list内部申请内存失败时,会抛出异常,然后临时对象就会析构,释放内存,完美。
如果emplace_back在list内部申请节点失败,这个时候new Widget申请到的内存不会被回收导致内存泄漏。
为了解决这个问题,可以改成如下方式:
std::shared_ptr<Widget> spw(new Widget, killWidget); ptrs.emplace_back(std::move(spw))
但是这里还是要进行构造和析构spw,置入并没有什么性能优势。
2. 置入函数可能会执行在插入函数中被拒绝的类型转换
std::regex的构造函数带有explicit的声明,这就导致了置入与插入的区别,见下面代码:
std::regex r1 = nullptr; // 错误,由于存在explicit,无法实现nullptr到std::regex的隐式转换 regexes.push_back(nullptr); // 错误,由于存在explicit,无法实现nullptr到std::regex的隐式转换 std::regex r2(nullptr); // 能编译 regexes.emplace_back(nullptr); // 能编译
对于push_back采用的是第一种方式赋值构造的方式,会将nullptr这个非法参数拒绝。而emplace_back采用的是第二种直接初始化的方式,会接受nullptr这个可能非法的参数。