变参模板和完美转发(Variadic Template and Perfect Forwarding)以及一些关于vector 和 list 添加元素效率的比较

目录

写在前面

关键字

效率问题

致谢


写在前面

最近,由于忙于论文、会议和组会,我的 C++ 学习和图形学系列《Games》暂时搁置了。现在,我回来继续分享一些有趣的内容。这一次,我将介绍 C++11 的两个强大新特性:变参模板和完美转发,以及比较 listvector 在性能上的区别。还有就是,数据结构的部分很快就会开始,大家继续加油,共勉。

关键字
  1. Variadic Templates (变参模板)Perfect Forwarding (完美转发)都是 C++11 引入的新特性。

  2. Variadic Templates (变参模板)允许函数或类模板接受任意数量和类型的参数,提供了一种创建接受可变数量参数的模板的方法,减少了重载函数的需要,使得代码更加简洁和清晰。在C++11之前,实现这种功能通常需要重载函数或使用递归模板技巧。

  3. Perfect Forwarding (完美转发):与变参模板和引用折叠规则一起工作,允许你将函数的参数完整无缺地转发给另一个函数。这个特性主要通过引用折叠、std::forward 函数以及右值引用来实现,用于解决函数模板在参数传递过程中可能发生的值类别(左值、右值)改变的问题,完美转发允许函数模板保持参数的左值或右值特性。

  4. emplace() 是 C++ 标准库容器的成员函数之一,常见于如 std::vectorstd::liststd::map 等容器中。这个函数的主要作用是在容器内直接构造元素,而不是先构造然后复制或移动到容器中。它提供了一种更高效的方式来插入新元素,尤其是对于非基本数据类型的对象。

效率问题
  • 为什么我们要使用完美转发?

    • 保持原始参数属性:确保函数模板中的参数能以其原始形式(包括lvalue和rvalue)被转发,避免了不必要的拷贝或移动操作。

    • 提高效率:在处理资源敏感或性能关键的代码时,完美转发能够提高程序的效率。

    • 增强模板函数的通用性:使模板函数能够适用于更广泛的使用场景,无论参数是左值还是右值。

  • 左值和右值

  • 在 C++ 中,表达式可以产生两种类型的值:

    • 左值(Lvalue):指的是一个持久的、具有明确存储位置的对象。左值可以出现在赋值表达式的左侧或右侧。
    • 右值(Rvalue):通常是临时的、没有明确存储位置的对象。右值只能出现在赋值表达式的右侧。
  • 如何实现完美转发

    • 完美转发在 C++11 中通过引入右值引用和 std::forward 函数得以实现。

    • 右值引用:通过 T&& 声明,它可以绑定到右值,从而允许函数模板接收右值参数。

    • std::forward:这个函数模板用于保持参数的原始值类别(左值或右值)。当传递给 std::forward 的参数是一个右值时,std::forward 会将参数作为右值传递;否则,它会保持参数的左值特性。

  • 一个简单的例子,假设有一个接受任何类型参数的函数模板 forwarder,它将参数转发给另一个函数 :

    • void foo(int& x) { /* ... handling lvalue ... */ }
      void foo(int&& x) { /* ... handling rvalue ... */ }
      
      template <typename T>
      void forwarder(T&& x) {
          foo(std::forward<T>(x));
      }
      
    • 在这个代码中,当你调用 forwarder 并传递一个左值(如 int a; forwarder(a);)时,forwardera 作为左值传递给 foo

    • 当你传递一个右值(如 forwarder(42);)时,forwarder 将这个右值传递给 foo

    • 通过这种方式,forwarder 函数模板能够保持传递给它的参数的原始值类别,并准确地转发给 foo 函数,这就是完美转发的核心概念。

  • 对于一个容器来说在添加容器内容时可能会引发不必要的拷贝和移动操作,这会造成代码效率的降低,为此我们可以进行一个简单的测试。

  • 首先我们定义一个学生类:

    • 每个学生包含三个成员变量:姓名、年龄以及一个标识符用于判断对象创建自有参构造函数(0),拷贝构造(1)还是移动构造(2)。

    • 注意在移动构造被定义时,必须定义移动赋值和拷贝赋值。

    • 有参构造函数,拷贝函数以及析构函数,并在各个函数中输出其所在位置以及更新标识符,在两种构造函数中分别输出构造对象的信息,在析构函数中指出此对象是从哪一種构造函数构造而来,基于标识符。

    • 创建两个公有的方法用于获取学生的姓名和年龄。

    • 学生类的构造如下:

 #include <iostream>
 #include <cstring>
 #include <string>
 ​
 class Student {
 public:
     Student(int age, const std::string& name) : _age(age), _name(name), _flag(0) {
         std::cout << "Constructor is being called now" << std::endl;
         std::cout << "Name is " << _name << " Age is " << _age << std::endl;
     }
 ​
     Student(const Student& right) : _age(right._age), _name(right._name), _flag(1) {
         std::cout << "Copy constructor is being called now" << std::endl;
         std::cout << "Name is " << _name << " Age is " << _age << std::endl;
     }
 ​
     Student(Student&& right) noexcept : _age(right._age), _name(std::move(right._name)), _flag(2) {
         std::cout << "Move constructor is being called now" << std::endl;
         std::cout << "Name is " << _name << " Age is " << _age << std::endl;
     }
 ​
     Student& operator=(const Student& right) {
         if (this != &right) {
             _age = right._age;
             _name = right._name;
             _flag = 1;
         }
         return *this;
     }
 ​
     Student& operator=(Student&& right) noexcept {
         if (this != &right) {
             _age = right._age;
             _name = std::move(right._name);
             _flag = 2;
         }
         return *this;
     }
 ​
     [[nodiscard]] std::string getName() const {
         return _name;
     }
 ​
     [[nodiscard]] int getAge() const {
         return _age;
     }
 ​
     ~Student() {
         if (_flag == 0) {
             std::cout << "Destructor for constructor is being called now" << std::endl;
         } else if (_flag == 1) {
             std::cout << "Destructor for copy constructor is being called now" << std::endl;
         } else {
             std::cout << "Destructor for move constructor is being called now" << std::endl;
         }
     }
 ​
 private:
     int _age;
     std::string _name;
     int _flag;
 };
 ​
  • 现在我们来思考添加自定义类对象至容器中的几种方法,我们首先以vector容器为例:

  • 第一种方法,先创建一个对象然后把这个对象添加至容器中:

  •  #include <iostream>
     #include <cstring>
     #include <vector>
     ​
     void test_4_inserting_m1(){
         std::vector<Student> Vstu;
         //insert student method
         //method 1, first define an object then insert it
         Student st1(18, "gulgun");
         Vstu.push_back(st1);
     }
     ​
     int main() {
         test_4_inserting_m1();
     ​
         return 0;
     }

  • 最终输出结果如下:

  •  /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project02/cmake-build-debug/project02
     constructor is being called now
     name is gulgun age is 18
     copy constructor is being called now
     name is gulgun age is 18
     the destructor for constructor is being called now
     the destructor for copy constructor is being called now
     ​
     Process finished with exit code 0
     ​
  • 可以看出,在添加对象st1时vector调用了一个拷贝构造函数,这是一个深拷贝,即添加元素之后对此vector元素进行修改不会影响原来的元素。

  • 那么我们来试一下第二种方法,即直接在push_back()中添加一个临时变量:

  •  #include <iostream>
     #include <cstring>
     #include <vector>
     ​
     void test_4_inserting_m2(){
         std::vector<Student> Vstu;
         //insert student method
         //method 2,directly insert one temporary object
     ​
         Vstu.push_back(Student(18, "gulgun"));
     }
     ​
     int main() {
         test_4_inserting_m2();
     ​
         return 0;
     }

  • 最后输出结果如下:

  •  
    /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project02/cmake-build-debug/project02
     Constructor is being called now
     Name is gulgun Age is 18
     Move constructor is being called now
     Name is gulgun Age is 18
     Destructor for constructor is being called now
     Destructor for move constructor is being called now
     ​
     Process finished with exit code 0

  • 可以看出和之前类似,即使直接传入临时对象,在添加元素的瞬间调用了移动构造函数(如果没有定义移动构造函数就会自动调用拷贝构造函数)。

  • 试一下第三中方法,使用emplace_back()并传入临时对象。

  •  
    #include <iostream>
     #include <cstring>
     #include <vector>
     ​
     void test_4_inserting_m3(){
         std::vector<Student> Vstu;
         //insert student method
         //method 3,directly insert one temporary object using emplace_back()
         Vstu.emplace_back(Student(18, "gulgun"));
     }
     ​
     int main() {
         test_4_inserting_m3();
     ​
         return 0;
     }

  • 输出结果如下:

  •  /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project02/cmake-build-debug/project02
     Constructor is being called now
     Name is gulgun Age is 18
     Move constructor is being called now
     Name is gulgun Age is 18
     Destructor for constructor is being called now
     Destructor for move constructor is being called now
     ​
     Process finished with exit code 0

  • 和push_back()传入临时对象一样,还是调用了移动构造函数,如果没有定义移动构造这里就会调用拷贝构造函数,可见完美转发还是没有被实现。

  • 下面是完美转发的调用:

  • 直接传入初始化参数至emplace_back()

  •  #include <iostream>
     #include <cstring>
     #include <vector>
     ​
     void test_4_inserting_m4(){
         std::vector<Student> Vstu;
         //insert student method
         //method 4, perfect forward
         Vstu.emplace_back(18, "gulgun");
     }
     ​
     int main() {
         test_4_inserting_m4();
     ​
         return 0;
     }
     ​

  • 输出结果如下:

  • /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project02/cmake-build-debug/project02
    Constructor is being called now
    Name is gulgun Age is 18
    Destructor for constructor is being called now
    
    Process finished with exit code 0

  • 可以看到并没有调用任何多余的拷贝构造和移动构造函数,这就是完美转发和变参模板的意义。

  • 还可以指定位置进行完美转发(类似于insert方法):

  • void test_4_inserting_m5(){
        std::vector<Student> Vstu;
        //insert student method
        Vstu.emplace(Vstu.begin(), 1, "panghu");
    }
    
    int main() {
        test_4_inserting_m5();
    
        return 0;
    }

  • 输出结果如下:

  • /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project02/cmake-build-debug/project02
    Constructor is being called now
    Name is panghu Age is 1
    Destructor for constructor is being called now
    
    Process finished with exit code 0

  • 依旧没有调用任何多余的拷贝和移动构造函数。

  • 当我们连续添加多个元素时可能会发生一些问题:

  • void test_4_inserting_m6(){
        std::vector<Student> Vstu;
        Vstu.emplace_back(Student(18, "gulgun"));
        Vstu.emplace(Vstu.begin(), 1, "panghu");
    }
    
    int main() {
        test_4_inserting_m6();
    
        return 0;
    }

  • 输出结果如下:

  • /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project02/cmake-build-debug/project02
    Constructor is being called now
    Name is gulgun Age is 18
    Move constructor is being called now
    Name is gulgun Age is 18
    Destructor for constructor is being called now
    Constructor is being called now
    Name is panghu Age is 1
    Move constructor is being called now
    Name is gulgun Age is 18
    Destructor for move constructor is being called now
    Destructor for constructor is being called now
    Destructor for move constructor is being called now
    
    Process finished with exit code 0

  • 一下子出现了太多的信息,我们来添加几个辅助输出好好理解一下,继续在这个函数上面修改一下:

    • 首先我们通过完美转发添加一个元素至容器中

    • 打印当前容器的容量

    • 完美转发添加另外一个元素至容器中

    • 继续打印当前容器的容量

    • 输出容器内的元素

  • void test_4_inserting_m3(){
        std::vector<Student> Vstu;
        //insert student method
        Vstu.emplace_back(18, "gulgun");
        
        std::cout << "the current capacity is " << Vstu.capacity() << std::endl;
        //Vstu.emplace_back( 1, "panghu");
        Vstu.emplace(Vstu.begin(), 1, "panghu");
        std::cout << "enlarge the capacity of the vector and copy the previous objects in the container" << std::endl;
        std::cout << "the current capacity is " << Vstu.capacity() << std::endl;
        std::cout << "here is the printing operation" << std::endl;
        for(const auto& item: Vstu){
            std::cout << item.getName() << ":" << item.getAge() << std::endl;
        }
        std::cout << "here is the end of printing operation" << std::endl;
    }

  • 修改后的运行输出如下:

  • /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project02/cmake-build-debug/project02
    Constructor is being called now
    Name is gulgun Age is 18
    the current capacity is 1
    Constructor is being called now
    Name is panghu Age is 1
    Move constructor is being called now
    Name is gulgun Age is 18
    Destructor for constructor is being called now
    enlarge the capacity of the vector and copy the previous objects in the container
    the current capacity is 2
    here is the printing operation
    panghu:1
    gulgun:18
    here is the end of printing operation
    Destructor for constructor is being called now
    Destructor for move constructor is being called now
    
    Process finished with exit code 0

  • 我们来好好地理解一下这个输出:

    • 首先是第一个元素被完美转发至vector,因此只有一个构造函数被调用,此时capacity为1。

    • 然后是一个新的完美转发被执行,由于capacity不足,需要重新开辟一段内存空间然后把之前的所有的内容复制进去,但是第二个元素的转发被执行所以调用了一次构造函数。

    • 构造万元素后需要把原来的元素复制一份过来,于此同时原来位置的元素会被销毁,于是便有了这个移动构造和一个由构造函数创建的对象的析构函数的调用。

    • 然后就是笔者输出的一些信息,显示了经过两次完美转发后其实容器的容量实在变化的,变成2了。

    • 然后就是打印出容器内部的信息,按照我们的想法,第二个被插入的元素排在第一位所以这个输出是正确的。

    • 最后就是程序调用结束两个对象的析构函数也被调用了,而且按照栈数据堆叠的规则被依次析构销毁。

  • 由此可见这一段复制或者移动其实是因为vector容器本身的特性导致的,而不是完美转发的问题。那么我们就会思考,如果不是使用这种需要重新开辟空间的数据结构类型是否可以避免这种无意义的频繁拷贝,从而最大限度地优化我们的性能呢?

  • 因为我们知道list中通过指针寻址并不需要开辟一段连续的内存,因此也就不存在有拷贝的潜在问题,所以不妨采用list数据结构对以上代码进行优化,简单把容器换成list,但是这里需要说明的是list中没有capacity方法,于是笔者在这里做了一些简单的修改:

  •  
    
    #include <iostream>
     #include <cstring>
     #include <list>
     ​
     void test_4_inserting_list(){
         std::list<Student> lstu;
         lstu.emplace_back(18, "gulgun");
         lstu.emplace(lstu.begin(), 1, "panghu");
         std::cout << "no need to enlarge the capacity" << std::endl;
         std::cout << "no capacity for list " << std::endl;
         std::cout << "here is the printing operation" << std::endl;
             for(const auto& item: lstu){
                 std::cout << item.getName() << ":" << item.getAge() << std::endl;
             }
         std::cout << "here is the end of printing operation" << std::endl;
     ​
     }
     ​
     int main() {
         test_4_inserting_list();
         
         return 0;
     }
     ​
  • 最后的输出如下所示:

  •  /media/herryao/81ca6f19-78c8-470d-b5a1-5f35b4678058/work_dir/Document/computer_science/QINIU/projects/week04/mon/project02/cmake-build-debug/project02
     Constructor is being called now
     Name is gulgun Age is 18
     Constructor is being called now
     Name is panghu Age is 1
     no need to enlarge the capacity
     no capacity for list 
     here is the printing operation
     panghu:1
     gulgun:18
     here is the end of printing operation
     Destructor for constructor is being called now
     Destructor for constructor is being called now
     ​
     Process finished with exit code 0
     
  • 可以看出,完美转发的性能在这里展现无疑,依旧是每个元素只调用一次构造,且不会有重新开辟内存空间造成的复制和移动问题。

  • 再提及一点,我就不用代码解释了std::vector<int> obj(int)的这种开辟空间的构造方式会调用默认的构造函数直接添加元素在容器内部,注意这不是简单的开辟内存而是直接在里面放置了元素,因此如果这个时候添加元素进去还是会由于capacity不足重新开辟并复制或移动内容到新的内存中去。

  • 最后还是督促一下自己快点结束cpp的基础篇,然后尽快进入数据结构的部分。

致谢
  • 感谢Martin老师的课程。

  • 继续感谢各位的支持,希望各位不断变强,下一篇很快会来,应该是一个小小的项目实战。

  • 37
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值