微调
条款 41:如果参数可拷⻉并且移动操作开销低,考虑直接按值传递
-
对于可拷贝、移动开销低,且一定会被拷贝的形参而言,按值传递的效率基本和按引用传递一样,而且可能生成更少的目标代码。
-
当我们在编写构造函数时,考虑到对左值和右值需要区分对待(拷贝语义和移动语义),通常采用下面三种方法进行编写:
-
针对左值和右值分别重载
1 2 3 4 5 6 7 8 9 10 11
class Widget { //方法1:对左值和右值重载 public: void addName(const std::string& newName) { names.push_back(newName); } // rvalues void addName(std::string&& newName) { names.push_back(std::move(newName)); } … private: std::vector<std::string> names; };
优点:
- 左值对应一次拷贝构造,右值对应一次移动构造。
缺点:
- 代码冗余,同一份代码需要编写两个不同版本。
-
万能引用模板
1 2 3 4 5 6 7
class Widget { //方法2:使用通用引用 public: template<typename T> void addName(T&& newName) { names.push_back(std::forward<T>(newName)); } … };
优点:
- 性能高,左值对应一次拷贝构造,右值对应一次移动构造。
- 代码量少,简洁。
缺点:
- 作为模板,声明和实现必须都置于头文件中,且还可能根据不同的模板类型实例化出不同的模板函数,导致代码增大。
- 有些类型不能以万能引用传参,如果传入了这些类型则会导致报错。
-
按值传递
1 2 3 4 5 6
class Widget { //方法3:传值 public: void addName(std::string newName) { names.push_back(std::move(newName)); } … };
缺点:
- 性能低,无论传入的是左值还是右值,首先都需要对形参 newName 进行一次拷贝/移动构造,接着将构造出的 newName 作为右值传入容器中,对应一次移动构造。对于左值而言需要一次拷贝构造和一次移动构造,而对于右值而言则是两次移动构造。比起前两个方法,多了一次移动构造操作。
- 可能导致切片问题,这个下面会具体介绍。
优点:
- 只需要编写单个函数,且没有万能模板的一些隐患。
- 实现简单,代码简洁。
我们需要根据具体需求场景进行选择,当对性能要求高时,可以选择使用重载或者万能引用模板。而参数可拷贝,且移动开销低时,就可以考虑使用按值传递。
-
-
-
通过构造函数拷贝参数可能比通过赋值拷贝开销大得多。
-
按值传递会导致切片问题,所以不适合基类类型的参数。
-
当将派生类的对象按值传递给基类时,由于类型不同,会自动进行隐式的类型转换,而在转换中则会对派生类对象进行切割,将派生类比基类多出的成员全部切割掉,以保证其能够转换为基类类型。
1 2 3 4 5 6 7 8
class Widget { … }; //基类 class SpecialWidget: public Widget { … }; //派生类 void processWidget(Widget w); //对任意类型的Widget的函数,包括派生类型 … //遇到对象切片问题 SpecialWidget sw; … processWidget(sw); //processWidget看到的是Widget, //不是SpecialWidget!
-
条款 42:使⽤ emplacement 代替 insertion
-
原则上,emplacement 函数有时比 insertion 高效,并且不会更差。
-
insertion 函数接受的参数是待插入对象,而 emplacement 接受的参数是待插入对象的构造函数参数,这就导致 emplacement 避免了临时对象的创建和析构,而 insertion 则无法做到。
例如:
1 2 3 4 5 6
std::vector<std::string> vs; //std::string的容器 vs.push_back("xyzzy"); //添加字符串字面量,改代码等价于下面这一行 vs.push_back(std::string("xyzzy")); //创建临时std::string,把它传给push_back vs.emplace_back("xyzzy"); //直接用“xyzzy”在vs内构造std::string
-
-
实际上,当满足以下条件时,emplacement 函数更快:
- 待添加的值是以构造而非赋值的方式加入容器。
- 当以构造的方式放入容器时,insertion 就有可能创建和析构临时对象。
- 传入的实参类型与容器类型不一致。
- 当传入的实参类型与容器类型不一致时,如果使用 insertion 则需要创建一个该类型的临时对象,并进行隐式类型转换,之后还要析构临时对象。
- 容器不会拒绝已经存在的重复值。
- 为了判断一个元素是否已存在于容器中,emplacement 需要创建一个具有新值的节点,以便将该节点的值与容器中现有节点的值进行比较。如果该元素不存在,则将其 emplacement。而如果存在,则终止 emplacement,并将节点析构,这时就浪费了构造和析构的成本。
- 待添加的值是以构造而非赋值的方式加入容器。
-
emplacement 函数可能会执行 insertion 函数中被拒绝的类型转换。
-
在 emplacement 使用的是直接初始化,所以它能够调用带有 explicit 声明的构造函数,而使用 insertion 函数时使用的是拷贝初始化,所以它不能调用带有 explicit 声明的构造函数。这就导致 emplacement 可能存在隐式的类型转换。
1 2 3 4 5 6 7 8
std::vector<std::regex> regexes; regexes.push_back(nullptr); //错误!拷贝初始化不允许用那个构造函数 regexes.emplace_back(nullptr); //可编译。直接初始化允许使用接受指针的 //std::regex的explicit构造函数 //上面的代码等价于隐式构造了一个regex对象 std::regex r(nullptr);
-