Effective Modern C++ 读书笔记(七):微调

微调

条款 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. 左值对应一次拷贝构造,右值对应一次移动构造。

        缺点

        1. 代码冗余,同一份代码需要编写两个不同版本。
      • 万能引用模板

        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. 代码量少,简洁。

        缺点:

        1. 作为模板,声明和实现必须都置于头文件中,且还可能根据不同的模板类型实例化出不同的模板函数,导致代码增大。
        2. 有些类型不能以万能引用传参,如果传入了这些类型则会导致报错。
      • 按值传递

        1
        2
        3
        4
        5
        6
        
        class Widget {                                  //方法3:传值
        public:
            void addName(std::string newName)
            { names.push_back(std::move(newName)); }
            
        };
        

        缺点:

        1. 性能低,无论传入的是左值还是右值,首先都需要对形参 newName 进行一次拷贝/移动构造,接着将构造出的 newName 作为右值传入容器中,对应一次移动构造。对于左值而言需要一次拷贝构造和一次移动构造,而对于右值而言则是两次移动构造。比起前两个方法,多了一次移动构造操作。
        2. 可能导致切片问题,这个下面会具体介绍。

        优点:

        1. 只需要编写单个函数,且没有万能模板的一些隐患。
        2. 实现简单,代码简洁。

      我们需要根据具体需求场景进行选择,当对性能要求高时,可以选择使用重载或者万能引用模板。而参数可拷贝,且移动开销低时,就可以考虑使用按值传递。

  • 通过构造函数拷贝参数可能比通过赋值拷贝开销大得多。

  • 按值传递会导致切片问题,所以不适合基类类型的参数。

    • 当将派生类的对象按值传递给基类时,由于类型不同,会自动进行隐式的类型转换,而在转换中则会对派生类对象进行切割,将派生类比基类多出的成员全部切割掉,以保证其能够转换为基类类型。

      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);               
      
Built with Hugo
主题 StackJimmy 设计