【C++11】initializer_list列表初始化、右值引用和移动语义、可变参数模版等



前言

本文主要介绍C++11中新增的一些重要语法:包括initializer_list列表初始化、右值引用和移动语义、引用折叠、万能引用、完美转发、可变参数模版、emplace系列接口等。


一、简介一下C++11

  • C++11(曾用名 C++0x)是 C++ 编程语言的一次里程碑式更新,于 2011 年 8 月正式发布。它不仅修复了 C++98/03 标准中的诸多缺陷,更引入了大量现代编程语言特性,极大地提升了代码的可读性、安全性和开发效率,彻底改变了 C++ 的编程范式,为后续的 C++14、C++17 等标准奠定了基础。
  • C++03与C++11期间花了8年时间,故而这是迄今为为最⻓的版本间隔。从那时起,C++就有规律地每3年更新一次。
  • (补:上图中c++版本字体越大说明更新幅度越大)

二、{}列表初始化

1.c++98传统的{}

C++98中一般数组和结构体可以用 {} 进行初始化。

cpp 复制代码
#include <iostream>
using namespace std;

struct Point
{
	int _x;
	int _y;
};

int main()
{
	//c++98支持的{}初始化
	int array1[] = { 1, 2, 3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };

	return 0;
}

2.c++11的{}

  • C++11以后想统一初始化方式,试图实现⼀切对象皆可用 {} 初始化,{}初始化也叫做列表初始化。
  • 内置类型支持,自定义类型也支持,自定义类型本质是隐式类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
        :_year(year)
        , _month(month)
        , _day(day)
    {
        cout << "Date(int year, int month, int day)" << endl;
    }
    Date(const Date& d)
        :_year(d._year)
        , _month(d._month)
        , _day(d._day)
    {
        cout << "Date(const Date& d)" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};


int main()
{
    //c++11{}支持内置类型
    //不过也没人这么用
    int x = { 1 };

    //自定义类型
    //这里本质是用 {2025,9,1} 构造一个临时对象
    //再用临时对象拷贝构造 d1
    //编译器合二为一优化成直接构造,也就是没有临时对象和拷贝构造了
    Date d1 = { 2025, 9, 1 };

    //那么这里就是引用临时对象了
    const Date& d2 = { 2024, 9, 1 };

    //需要注意的是c++98支持单参数构造,也可以不用{}
    //前提是得有默认构造
    Date d3 = { 2025 };
    Date d4 = 2024;

    return 0;
}

运行结果:

  • {}初始化的过程中,可以省略掉=
  • C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很方便。
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
        :_year(year)
        , _month(month)
        , _day(day)
    {
        cout << "Date(int year, int month, int day)" << endl;
    }
    Date(const Date& d)
        :_year(d._year)
        , _month(d._month)
        , _day(d._day)
    {
        cout << "Date(const Date& d)" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    //可以省略=
    int x1{ 1 };
    Date d1{ 2025,9,1 };
    const Date& d2{ 2024,9,1 };

    //比起有名对象和匿名对象传参,这里直接使用{}更方便
    vector<Date> v1;
    v1.push_back({ 2025,10,1 });

    return 0;
}

3.c++11中的std::initializer_list

  • 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如⼀个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持。
  • C++11库中提出了一个std::initializer_list的类,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。std::initializer_list⽀持迭代器遍历。
  • 容器支持⼀个std::initializer_list的构造函数,也就支持任意多个值构成的 {x1,x2,x3 ...}进行初始化。。STL中的容器支持任意多个值构成的进行初始化,就是通过 std::initializer_list的构造函数支持的。可以通过文档查到很多容器都支持std::initializer_list进行列表初始化构造或者赋值。
cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
#include <map>
using namespace std;

int main()
{
	auto it = { 1,2,3,4,5,6,7 };
	for (auto& e : it)
	{
		cout << e << " ";
	}
	cout << endl << typeid(it).name() << endl << endl;

	//直接使用initializer_list进行初始化列表赋值构造
	vector<int> v1(it);
	list<int> l1({ 1,2,3,4,5,6 });
	map<int, int> m1({ {1,1},{2,2},{3,3 } });
    
	return 0;
}

运行结果:


三、右值引用和移动语义

C++98的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

1.左值和右值

  • ++左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中++ ,我们可以获取它的地址 ,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址。
  • ++右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,++ 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
  • 值得⼀提的是,左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代C++中,lvalue被解释为loactorvalue的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而rvalue被解释为readvalue,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main()
{
	//左值:可以取地址
	//以下的p、b、c、*p、s、s[0]都是常见的左值
	int* p = new int(1);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("xxxxxxx");
	s[0] = '1';

	//左值都可以取地址
	cout << p << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &(*p) << endl;
	cout << &s << endl;
	cout << (void*)&s[0] << endl;


	//右值:不可以取地址
	double x = 1.1, y = 2.2;
	//以下的10、x+y、fmin(x,y)、string("111111")都是一些常见的右值
	10;
	x + y;
	fmin(x, y);
	string("111111");

	//无法取地址,会报错
	/*cout << &10 << endl;
	cout << &(x+y) << endl;
	cout << &(fmin(x, y)) << endl;
	cout << &string("11111") << endl;*/

	return 0;
}

我对右值的理解是那些临时对象,比如x+y的结果是存储在一个临时对象中,fmin的返回值也是一个临时对象。string("111111")则本身是一个匿名对象,10则是一个字面量本身。很明显它们的生命周期只有它们存在的那一行,是一些即将消亡的值。
详细的左值右值分类可访问该网站: Value categories - cppreference.com

  • 该网站中对右值进行了分类,右值被划分纯右值(purevalue,简称prvalue)和将亡值 (expiring value,简称xvalue)。
  • 纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达式(如move)。
  • 还有泛左值(generalizedvalue,简称glvalue),泛左值包含将亡值和左值。
  • 该分类仅做了解即可,感兴趣可以访问网址阅读。

2.左值引用和右值引用

  • **Type& r1 = x; Type&& rr1 = y;**第一个语句就是左值引用,左值引用就是给左值取别 名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main()
{
	//左值:可以取地址
	//以下的p、b、c、*p、s、s[0]都是常见的左值
	int* p = new int(1);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("xxxxxxx");
	s[0] = '1';

	//左值引用给左值取别名
	int*& r1 = p;
	int& r2 = b;
	const int& r3 = c;
	int& r4 = *p;
	string& r5 = s;
	char& r6 = s[0];


	//右值引用给右值取别名
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("111111");

	return 0;
}

  • 左值引用不能直接引用右值,但是const左值引用可以引用右值。
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main()
{
	//const左值引用给右值取别名
	double x = 1.1, y = 2.2;
	const int& r1 = 10;
	const double& r2 = x + y;
	const double& r3 = fmin(x, y);
	const string& r4 = string("111111");

	return 0;
}

  • 右值引用不能直接引用左值,但是右值引用可以引用move(左值)。
  • move是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知识,这个我们下面会细讲。
cpp 复制代码
#include <iostream>
#include <string>
#include <utility>
using namespace std;

int main()
{
	//左值:可以取地址
	int* p = new int(1);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("xxxxxxx");

	//可以用move将左值属性转为右值,再用右值引用取别名
	int*&& rr1 = move(p);
	int&& rr2 = move(b);
	const int&& rr3 = move(c);
	int&& rr4 = move(*p);
	string&& rr5 = move(s);
	string&& rr6 = (string&&)s;//这样也能侧面证明move本质是一个强制类型转换

	return 0;
}

补:move属于<utility>头文件,如果编译器识别不到move可以包一下这个头文件


  • 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。(右值引用本身属性是左值
  • 语法层⾯看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要揉到一起去理解,互相佐证,这样反而是陷入迷途。
cpp 复制代码
#include <iostream>
#include <string>
#include <utility>
using namespace std;

int main()
{
	//右值引用本身(rr1)是左值属性
	string&& rr1 = string("xxxxxxxx");
	cout << &rr1 << endl;

	//所以如果还要对rr1取别名,可以用move强转一下,或者const左值引用
	const string& r1 = rr1;
	string&& rr2 = move(rr1);

	return 0;
}
  • 右值引用本身是左值,这样就可以使用右值引用进行管理操控资源,之前说临时对象具有常性是因为没有主体对其进行操控,现在有了右值引用,那么这些临时对象就能够进行修改了。

3.引用延迟生命周期

  • 右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改(右值引用支持修改)。
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main()
{
	//可以延长临时对象或者匿名对象的生命周期
	const string& r1 = string("xxxxx");
	string&& rr1 = string("1111111");

	
	//r1 += "1111";//这里const左值引用不能修改
	rr1 += "xxxx";//右值引用是支持修改的
	cout << rr1 << endl;

	return 0;
}

运行结果:


4.左值和右值的参数匹配

  • C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
  • C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的 f函数,那么实参是左值会匹配 f(左值引用),实参是const左值会匹配 f(const左值引用),实参是右值会匹配 f(右值引用)。
  • 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉跟怪,下一节我们讲右值引用的使用场景和引用折叠后,就能体会这样设计的价值了。
cpp 复制代码
#include <iostream>
#include <string>
#include <utility>
using namespace std;

void f(int& x)
{
	cout << "左值引用重载 f(int& x)" << endl;
}

void f(const int& x)
{
	cout << "const左值引用重载 f(const int& x)" << endl;
}

void f(int&& x)
{
	cout << "右值引用重载 f(int&& x)" << endl;
}

int main()
{
	int i = 1;
	const int ci = 2;

	f(i);	//调用f(int x)
	f(ci);	//调用f(const int& x)
	f(3);	//调用f(int&& x)
	f(move(i));	//调用f(int&& x)

	cout << endl;

	//右值引用本身属性是左值
	int&& rr1 = 4;
	f(rr1);	//调用f(int& x);
	f(move(rr1)); //调用f(int&& x);

	return 0;
}

四、右值引用和移动语义的使用场景

先剧透:右值引用和移动语义的最主要功能就是提效,下面我们注重理清楚它是怎么提效的

1.回顾左值引用使用场景,以及存在的问题

  • 左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。
  • 左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如下面的 addStrings 和 generate 函数,原因是函数结束后其函数栈帧会被销毁,其中产生的局部对象都会调用析构一起被释放。
  • C++98中的解决方案只能是被迫使用输出型参数解决,但是很明显,传值返回会调用拷贝构造,当函数返回的数据量非常大时,这种传值返回的效率是非常低下的。
cpp 复制代码
//计算数字字符串num1和numl的和
class Solution1
{
public:
    //传值返回需要拷贝
    string addStrings(string num1, string num2) 
    {
        string str;
        int end1 = num1.size() - 1, end2 = num2.size() - 1;

        //进位
        int next = 0;
        while (end1 >= 0 || end2 >= 0)
        {
            int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
            int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
            int ret = val1 + val2 + next;
            next = ret / 10;
            ret = ret % 10;
            str += ('0' + ret);
        }
        if (next == 1)
            str += '1';

        reverse(str.begin(), str.end());
        return str;
    }
};

//打印杨辉三角
class Solution2 {
public:
    // 这里的传值返回拷贝代价就太大了
    vector<vector<int>> generate(int numRows)
    {
        vector<vector<int>> vv(numRows);
        for (int i = 0; i < numRows; ++i)
        {
            vv[i].resize(i + 1, 1);
        }
        for (int i = 2; i < numRows; ++i)
        {
            for (int j = 1; j < i; ++j)
            {
                vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
            }
        }

        return vv;
    }
};
  • 那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法改变对象已经析构销毁的事实。(引用不能延长函数内部局部对象的生命周期,因为该局部对象是存储在函数的栈帧里,函数消亡局部对象也消亡)
  • C++11解决方法就是实现移动语义:使用移动构造和移动赋值。

2.移动构造和移动赋值

什么是移动语义: C++11 移动语义的核心是:将一个对象的资源所有权(如内存、文件句柄)* 直接 "转移" 给另一个对象,而非拷贝资源,从而避免不必要的内存拷贝,提升性能。实现移动语义依赖两个关键机制:右值引用、移动构造和移动赋值。*

移动构造和移动赋值:

  • 移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
  • 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
  • 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要"窃取"引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的txp::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。
  • 补充:c++11后,基本所有的STL容器都增加了移动构造和移动赋值

首先为了便于观察和实现,我们需要自己实现一个string类,这个类我们前面文章实现过就不多叙述了;然后为了避免重复展现实现的string类,这里先将完整的包含移动构造和移动赋值的string类代码展示出来:

mystring.h:

cpp 复制代码
#pragma once
#include <iostream>
#include <cassert>
using namespace std;

namespace txp
{
    class string
    {
    public:
        //迭代器
        typedef char* iterator;
        typedef const char* const_iterator;

        iterator begin()
        {
            return _str;
        }
        iterator end()
        {
            return _str + _size;
        }
        const_iterator begin() const
        {
            return _str;
        }
        const_iterator end() const
        {
            return _str + _size;
        }

        //默认构造
        string(const char* str = "")
            :_size(strlen(str))
            , _capacity(_size)
        {
            cout << "string(char* str)构造" << endl;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }

        //底层指针交换
        void swap(string& s)
        {
            ::swap(_str, s._str);
            ::swap(_size, s._size);
            ::swap(_capacity, s._capacity);
        }

        //拷贝构造
        string(const string& s)
            :_str(nullptr)
        {
            cout << "string(const string& s) -- 拷贝构造" << endl;
            reserve(s._capacity);
            for (auto ch : s)
            {
                push_back(ch);
            }
        }

        //移动构造
        string(string&& s)
        {
            cout << "string(string&& s) -- 移动构造" << endl;
            swap(s);
        }

        //拷贝赋值
        string& operator=(const string& s)
        {
            cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
            if (this != &s)
            {
                _str[0] = '\0';
                _size = 0;
                reserve(s._capacity);
                for (auto ch : s)
                {
                    push_back(ch);
                }
            }
            return *this;
        }

        //移动赋值
        string& operator=(string&& s)
        {
            cout << "string& operator=(string&& s) -- 移动赋值" << endl;
            swap(s);
            return *this;
        }

        //析构
        ~string()
        {
            cout << "~string() -- 析构" << endl;
            delete[] _str;
            _str = nullptr;
        }

        //[]重载
        char& operator[](size_t pos)
        {
            assert(pos < _size);
            return _str[pos];
        }

        //扩容
        void reserve(size_t n)
        {
            if (n > _capacity)
            {
                char* tmp = new char[n + 1];
                if (_str)
                {
                    strcpy(tmp, _str);
                    delete[] _str;
                }
                _str = tmp;
                _capacity = n;
            }
        }

        //尾插
        void push_back(char ch)
        {
            if (_size >= _capacity)
            {
                size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newcapacity);
            }
            _str[_size] = ch;
            ++_size;
            _str[_size] = '\0';
        }

        //+=重载
        string& operator+=(char ch)
        {
            push_back(ch);
            return *this;
        }

        //返回C语言版底层字符串
        const char* c_str() const
        {
            return _str;
        }

        //返回字符串大小
        size_t size() const
        {
            return _size;
        }

    private:
        char* _str = nullptr;
        size_t _size = 0;
        size_t _capacity = 0;
    };

    //验证代码
    string addStrings(string num1, string num2)
    {
        string str;
        int end1 = num1.size() - 1, end2 = num2.size() - 1;
        int next = 0;
        while (end1 >= 0 || end2 >= 0)
        {
            int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
            int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
            int ret = val1 + val2 + next;
            next = ret / 10;
            ret = ret % 10;
            str += ('0' + ret);
        }

        if (next == 1)
            str += '1';
        reverse(str.begin(), str.end());
        cout << "******************************" << endl;
        return str;
    }
}

移动构造和移动赋值的实现:

cpp 复制代码
 //底层指针交换
 void swap(string& s)
 {
     ::swap(_str, s._str);
     ::swap(_size, s._size);
     ::swap(_capacity, s._capacity);
 }

 //移动构造
 string(string&& s)
 {
     cout << "string(string&& s) -- 移动构造" << endl;
     swap(s);
 }

//移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动赋值" << endl;
    swap(s);
    return *this;
}
  • 很明显,移动构造和移动赋值的实现并不复杂,除了参数变为右值引用,函数体的实现就是交换底层指针。这就很符合前面提到拷贝构造和移动赋值是"窃取"引用右值对象的资源,而非进行深拷贝。
  • 注意,移动构造和移动赋值后,通常形参中的右值引用的对象会被销毁或者置空,这是因为右值引用的一般都是临时对象之类(也有move等方式)的,而我们只需要临时对象中的数据,但又不想进行深拷贝,因此使用交换指针的方式进行"掠夺"资源,一般自己原指向的空间是空nullptr或者是不需要的数据,交换之后,形参部分就会带着不需要的数据或空数据自动销毁了,这样既完成了拷贝,效率又高,也不会导致内存泄漏。
  • 千万注意,移动构造和移动赋值只针对右值,也就是一些即将消亡的临时对象等值,对于左值还是老老实实进行拷贝构造或者拷贝赋值。

(vs2022)我们先观察一下参数匹配,以及强转s1进行移动构造后的情况:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <utility>
#include "mystring.h"

int main()
{
	//普通构造
	txp::string s1("xxxxxx");

	//拷贝构造
	txp::string s2 = s1;

	//构造+移动构造,编译器优化后变为直接构造
	txp::string s3 = txp::string("111111");

	//移动构造
	txp::string s4(move(s1));

	cout << endl << "*****************************" << endl << endl;

	return 0;
}

运行结果:

  • 首先这个运行结果是受编译器优化后的结果,稍后我会在Linux下展示编译器完全不进行优化的运行结果。
  • 然后我们先看一下将s1进行移动构造的前后结果:
  • 很明显,s1的资源被转移,并且自身变为空,跟被销毁了没什么两样(注意没有立即调用析构)。所以对于将左值强转成右值进行移动构造的情况中,我们需要谨慎,假如不希望s1置空就不要这样使用,当然本身就是临时对象的就没有这种顾虑。
  • 右值引用构造的意义:就是当传递的参数是右值时,也就是一些临时对象时,不需要进行拷贝之类的,直接将资源进行转移就行,当然移动构造和移动赋值是只针对右值的,左值是去匹配普通构造和普通赋值的,这就是它的用处,专门针对右值拷贝构造和拷贝赋值的场景。
  • 下面我们看在Linux下,上面代码完全不优化的运行结果,关闭优化指令:g++ -std=c++11 test.cpp -fno-elide-constructors(其中的test.cpp换成你的文件名)

(Linux)运行结果:

  • 上边的第一个析构就是临时对象string("111111")被移动构造后调用的析构。第二个构造也是string("111111")调用的构造,第一个移动构造是s3,第二个移动构造是s4。

3.右值引用和移动语义解决传值返回的问题

场景1:

cpp 复制代码
 //传值返回场景代码(两个数字字符串相加)
 string addStrings(string num1, string num2)
 {
     string str;
     int end1 = num1.size() - 1, end2 = num2.size() - 1;
     int next = 0;
     while (end1 >= 0 || end2 >= 0)
     {
         int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
         int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
         int ret = val1 + val2 + next;
         next = ret / 10;
         ret = ret % 10;
         str += ('0' + ret);
     }

     if (next == 1)
         str += '1';

     reverse(str.begin(), str.end());

     cout << "******************************" << endl;

     return str;
 }
cpp 复制代码
int main()
{
	txp::string ret = txp::addStrings("500", "20");
	cout << endl << ret.c_str() << endl;

	return 0;
}

(Linux无优化)运行结果:

  • 首先,虚线上面两组 构造+移动构造 是"500"和"20"传值给形参时进行的构造。剩余一个构造是str的构造。
  • 虚线下面的情况我们看下面这张图:
  • 下面我们注重解释移动构造的作用:
  • (注意此时讨论的都是编译器不优化的情况下)因为此时已经走到addStrings函数的末尾了,需要传值返回,传值方法是会产生临时对象的,为啥有临时对象呢,因为函数结束局部对象都会销毁,所以需要临时对象来储存函数的返回结果,这个临时对象需要被构造,假如我们没有实现移动构造,而只是实现了拷贝构造,那么这里的两次移动构造就都是拷贝构造了,想一想,如果数据量非常大编译器又没有优化,两次拷贝构造的消耗是非常大的,所以,当我们实现移动构造后,str因为是即将消亡的值,是被当做右值处理的,那么临时对象的构造就会调用移动构造,同理,这个临时对象本身也即将消亡,也是右值,所以ret调用的也是移动构造,这就是虚线下面两次移动构造的由来。
  • 移动构造本质就是交换底层指针嘛,所以基本没什么性能消耗,两次移动构造中间的析构则是str的析构。剩下的析构调用就是函数内部的以及临时对象的,最后一个析构就是ret的。
  • 所以经过这个场景我们应该能理解右值引用和移动构造最主要的意义------提效,就是提升效率。
  • 注意上面的str严格意义上应该是左值,但它又处于函数内部又即将消亡,编译器就将它识别为右值。

关于编译器的优化:

  • 因为C++11出来的比较晚,所以对于上面这样类似的传值返回场景等,早期的巨大性能消耗迫使编译器进行了优化,比如上面的代码在vs2022下的运行结果如下:
  • 因为vs2022的极致优化,很多需要连续拷贝构造的场景都被优化成直接构造。具体优化细节这里就不赘述了,前面类和对象的文章应该有提及到。
  • 那么既然编译器有优化,那我们为啥还要写移动构造和移动赋值呢?这就是需要考虑到代码的泛用性,换句话说你不能指望每个编译环境都有优化,另外,最主要的是有些场景会干扰编译器的优化,比如下面这个场景:

场景2:

cpp 复制代码
int main()
{
	txp::string ret;
	ret = txp::addStrings("500", "20");
	cout << endl << ret.c_str() << endl;

	return 0;
}
  • 其他什么都不变,仅仅只是将ret先定义再赋值,这时编译器就不得不调用拷贝赋值了,我们看vs2022的运行结果:

运行结果(VS2022):

  • 很明显,编译器是用到了移动赋值,这还是极致优化的结果,依旧调用了移动赋值,那么当我们没有实现移动赋值呢?我们看下面结果:
  • 很明显,将移动赋值注释后,调用的就是拷贝赋值,那么假如数据量非常大时,一次拷贝赋值的代价就很大了,所以你说有没有必要学习右值引用和移动语义,答案是肯定的,效率的提升是很明显的。
  • 总结就是一句话:不管编译器有没有优化,我们移动构造和移动赋值的效率依然很高。

最后注意主要是涉及深拷贝的情况,才有移动构造和移动赋值,因为浅拷贝本身消耗并不大


五、右值引用和移动语义在传参中的提效

  • 查看STL文档我们发现C++11以后很多容器的push和insert系列的接口都增加的右值引用版本。
  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象。
  • 当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上。

我们使用标准库中的list进行传参演示:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <utility>
#include <list>
#include "mystring.h"

int main()
{
	list<txp::string> lt;

	txp::string s1("xxxxxxxxxxx");
	lt.push_back(s1);
	cout << "*************************************" << endl;

	lt.push_back(txp::string("11111111"));
	cout << "*************************************" << endl;

	lt.push_back("22222222");
	cout << "*************************************" << endl;

	lt.push_back(move(s1));
	cout << "*************************************" << endl;

	return 0;
}

运行结果:

  • 运行结果很好的展示了,当传参的参数为右值时,就会调用移动构造为list中的元素进行构造。当传参为左值也就是第一个时,调用的就是拷贝构造,所以在传参这一块,右值明显效率高。

  • 接下来,我们把之前模拟实现的txp::list拷贝过来,当然以下实现的是精简版的,没有用上的功能都阉割了,然后我们自己实现右值引用版本的push_back和insert,看看其中具体的实现过程,加深理解。

**mylist.h:**先将完整版展示出来,再具体谈实现过程

cpp 复制代码
#pragma once
#include <utility>
using namespace std;

namespace txp
{
    //链表节点
    template<class T>
    struct ListNode
    {
        ListNode<T>* _next;
        ListNode<T>* _prev;
        T _data;

        //普通节点构造
        ListNode(const T& data = T())
            :_next(nullptr)
            , _prev(nullptr)
            , _data(data)
        {}

        //移动构造
        ListNode(T&& data)
            :_next(nullptr)
            , _prev(nullptr)
            , _data(move(data))
        {}
    };

    //链表迭代器
    template<class T, class Ref, class Ptr>
    struct ListIterator
    {
        typedef ListNode<T> Node;
        typedef ListIterator<T, Ref, Ptr> Self;
        Node* _node;

        ListIterator(Node* node)
            :_node(node)
        {}

        Self& operator++()
        {
            _node = _node->_next;
            return *this;
        }

        Ref operator*()
        {
            return _node->_data;
        }

        bool operator!=(const Self& it)
        {
            return _node != it._node;
        }
    };

    //链表
    template<class T>
    class list
    {
        typedef ListNode<T> Node;
    public:
        typedef ListIterator<T, T&, T*> iterator;
        typedef ListIterator<T, const T&, const T*> const_iterator;

        iterator begin()
        {
            return iterator(_head->_next);
        }

        iterator end()
        {
            return iterator(_head);
        }

        //初始化
        void empty_init()
        {
            _head = new Node();//哨兵位
            _head->_next = _head;
            _head->_prev = _head;
        }

        //构造
        list()
        {
            empty_init();
        }

        //普通尾插
        void push_back(const T& x)
        {
            insert(end(), x);
        }

        //右值引用版尾插
        void push_back(T&& x)
        {
            insert(end(), move(x));
        }

        //普通插入
        iterator insert(iterator pos, const T& x)
        {
            Node* cur = pos._node;
            Node* newnode = new Node(x);
            Node* prev = cur->_prev;
            // prev  newnode  cur
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = cur;
            cur->_prev = newnode;
            return iterator(newnode);
        }

        //右值引用版插入
        iterator insert(iterator pos, T && x)
        {
            Node* cur = pos._node;
            Node* newnode = new Node(move(x));
            Node* prev = cur->_prev;
            // prev  newnode  cur
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = cur;
            cur->_prev = newnode;
            return iterator(newnode);
        }
    private:
        Node* _head;
    };
}

现在,我们将具体新增的代码展示出来:

cpp 复制代码
namespace txp
{
    //链表节点
    template<class T>
    struct ListNode
    {
        //...
        
        //移动构造
        ListNode(T&& data)
            :_next(nullptr)
            , _prev(nullptr)
            , _data(move(data))
        {}
    };

    //... 
    
    //链表
    template<class T>
    class list
    {
        //...

        //普通尾插
        void push_back(const T& x)
        {
            insert(end(), x);
        }

        //右值引用版尾插
        void push_back(T&& x)
        {
            insert(end(), move(x));
        }

        //普通插入
        iterator insert(iterator pos, const T& x)
        {
            Node* cur = pos._node;
            Node* newnode = new Node(x);
            Node* prev = cur->_prev;
            // prev  newnode  cur
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = cur;
            cur->_prev = newnode;
            return iterator(newnode);
        }

        //右值引用版插入
        iterator insert(iterator pos, T && x)
        {
            Node* cur = pos._node;
            Node* newnode = new Node(move(x));
            Node* prev = cur->_prev;
            // prev  newnode  cur
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = cur;
            cur->_prev = newnode;
            return iterator(newnode);
        }

    private:
        Node* _head;
    };
}
  • 首先,想要实现右值引用版的push_back,那就必须新增一个参数为右值引用版本 void push_back(T&& x)
  • 然后就是第一个需要注意的点,因为push_back是复用insert进行尾插的,所以insert也必须新增一个右值引用版本的 iterator insert(iterator pos, T&& x),这还不是需要注意的点,需要注意的是push_bcak(T&& x)中的x虽然是右值引用的别名,但x本身还是左值属性,想要传参给右值引用版本的insert就必须使用move进行强转。
  • 然后是第二个需要注意的点,因为insert中需要创建新节点,也就是 new Node(x),new 会调用Node的构造函数,Node就是ListNode,所以我们需要实现ListNode的右值引用版本的构造函数,也就是实现移动构造ListNode(T&& data)
  • 然后这个 x 也是insert形参中右值引用的别名,这个x本身属性也是左值,所以第三点需要注意将右值引用版的insert中的 new Node(x) ->改为 new Node(move(x)),这样才能调用到移动构造。
  • 最后还有第四点需要注意,ListNode(T&& data)的移动构造中的形参 date,右值引用别名date的属性是左值,ListNode最终是需要调用T类型的移动构造,所以初始化列表中_data(data)想调用T类型的移动构造必须传右值才能匹配T类型的移动构造,当然前提是T类型支持移动构造,所以_data(data)->改为_data(move(data))
  • 这就是右值引用版本的push_back和insert的实现,有点复杂,但只需要记住一点,就是右值引用变量的别名属性是左值,往下面传递时想要保证右值属性并匹配右值引用参数,就需要使用move进行强制转换。

使用我们自己实现的list和自己的string进行演示:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <utility>
#include "mylist.h"
#include "mystring.h"

int main()
{
	txp::list<txp::string> lt;

	txp::string s1("xxxxxxxxxxx");
	lt.push_back(s1);
	cout << "*************************************" << endl;

	lt.push_back(txp::string("11111111"));
	cout << "*************************************" << endl;

	lt.push_back("22222222");
	cout << "*************************************" << endl;

	lt.push_back(move(s1));
	cout << "*************************************" << endl;

	return 0;
}

运行结果:

  • 注意开头的构造、拷贝构造、析构,是list构造函数调用初始化函数empty_init中new Node()创建哨兵位所进行的操作。new Node()需要调用ListNode的构造,因为是无参,所以ListNode使用缺省参数T(),T()就会调用string的构造,然后_date(date)就是涉及拷贝构造,最后T()构造的临时对象析构。这就是开头的三个调用操作。
  • 其余的和标准库中的list调用一样,就不细说了。

六、引用折叠和完美转发

在学习可变参数模板前,我们需要了解一些机制

1.引用折叠

  • C++中不能直接定义引用的引用如 int& && r = i;,这样写会直接报错。
  • 但是通过模板或typedef中的类型操作可以构成引用的引用,这就是引用折叠。
  • 通过模板或typedef中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用

这个引用折叠的规则我们先通过typedef类型操作展示一下:

cpp 复制代码
int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;

	//引用折叠
	lref& r1 = n;	//r1的类型是 int&
	lref&& r2 = n;	//r2的类型是 int&
	rref& r3 = n;	//r3的类型是 int&
	rref&& r4 = 1;	//r4的类型是 int&&

	return 0;
}
  • 很明显,对于左值引用lref,lref加上左值引用还是左值引用,lref加上右值引用还是左值引用,总结:左值引用加上任何引用都是左值引用。
  • 对于右值引用rref,rref加上左值引用是左值引用,rref加上右值引用则是右值引用,总结:右值引用只有加上右值引用才折叠为右值引用,加上左值引用就是左值引用,这一点和左值引用的总结相呼应。
  • 以上就是引用的折叠规则了。

现在我们继续看在函数模板的类型操作下的引用折叠:

cpp 复制代码
#include <iostream>
using namespace std;

//左值引用
template<class T>
void f1(T& x)
{}

//万能引用
template<class T>
void f2(T&& x)
{}

int main()
{
	int n = 0;

	f1<int>(n);//没有折叠->实例化为void f1(int& x)
	//f1(0);报错

	f1<int&>(n);//折叠->实例化为void f1(int& x);
	//f1<int&>(0);报错

	f1<int&&>(n);//折叠->实例化为void f1(int& x);
	//f1<int&&>(0);报错

	//折叠->实例化为void f1(const int& x);
	f1<const int&>(n);
	f1<const int&>(0);

	//折叠->实例化为void f1(const int& x);
	f1<const int&&>(n);
	f1<const int&&>(0);

	//------------------------------------

	f2(n);
	f2<int>(0);//没有折叠->实例化为void f2(int&& x);

	f2<int&>(n);//折叠->实例化为void f2(int& x);
	f2(0);

	f2(n);
	f2<int&&>(0);//折叠->实例化为void f2(int&& x);

	//折叠->实例化为void f2(const int& x);
	f1<const int&>(n);
	f1<const int&>(0);

	f2(n);
	f2<const int&&>(0);//折叠->实例化为void f2(const int&& x);

	return 0;
}
  • 很明显,当函数模板参数为引用类型时,只要传递的参数是引用类型的,就会触发引用折叠,折叠的规律依旧是:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用
  • 函数模板void f1(T& x),因为本身是左值引用,所以传什么都会变为左值引用。
  • 但是f2不一样,就是函数模板void f2(T&& x),为什么给它标注为万能引用,因为它接收左值引用就是左值引用,它接收右值引用就是右值引用。当然正常接收右值也是右值引用。
  • 当然,以上例子都是手动实例化,现实中也不会这样传参,接下来我们让万能函数模板自动实例化,观察万能引用的一些现象。

万能引用函数模板:

cpp 复制代码
#include <iostream>
#include <utility>
using namespace std;

template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	x++;

	//观察地址判断x是不是a的引用别名
	cout << &a << endl;
	cout << &x << endl << endl;
}

int main()
{
	//10是右值,推导出T为int,模版实例化为void Function(int&& t)
	Function(10);//打印出的地址不同,佐证了T为int类型

	//a是左值,推导出T为int&,模板实例化为void Function(int& t)
	int a;
	Function(a);//打印出的地址相同,佐证了T为int&类型

	//move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(move(a));//打印出的地址不同,佐证了T为int类型

	return 0;
}

运行结果:

  • 结合运行结果以及注释,我们可以发现一个问题,就是传右值给万能引用模板,虽然最终形参类型为右值引用,但是T本身却是int类型。
  • 然后传左值给万能引用模板是,T本身会变为int&类型。当然形参类型依旧是左值引用。
  • 我们接着观察const类型的引用,然后再总结。

const类型参数传参给万能引用函数模板:

cpp 复制代码
#include <iostream>
#include <utility>
using namespace std;

template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	//x++;  const无法修改

	//观察地址判断x是不是a的引用别名
	cout << &a << endl;
	cout << &x << endl << endl;
}

int main()
{
	//b是const左值,推导出T为const int&,模板实例化为void Function(const int& t)
	const int b = 1;//因为T是const修饰,所以不能x++
	Function(b);//然后地址依旧相同,佐证T为const int&类型

	//move(b)是const右值,推导出T为const int,模板实例化为void Function(const int&& t)
	Function(move(b));//同样,x不能修改,地址不同,佐证T为const int类型

	return 0;
}

运行结果:

  • 除了多了个const修饰,规律和上面一样。
  • **总结:**万能引用模板传参时( template<class T>void Function(T&& t) )
  1. 当传递左值时,T会识别为数据类型的左值引用,传递const类型时,T会识别为const数据类型的左值引用。
  2. 当传递右值时,T会识别为数据类型本身的类型,传递const类型时,T会识别为const数据类型本身的类型。
  • 这一点是新发现,也是一个容易混淆的点,至于为啥这样规定应该是语法规定,我们不用管。然后形参的类型依旧参考引用折叠的规律。

2.完美转发

以上引用折叠就是为了引出万能引用,但是万能引用存在一些问题,比如下面这种情况:

cpp 复制代码
#include <iostream>
#include <utility>
using namespace std;

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const右值引用" << endl; }

//万能引用
template<class T>
void Function(T&& t)
{
	Fun(t);
}

int main()
{
	//10是右值,模版实例化为void Function(int&& t)
	Function(10);//打印出的地址不同,佐证了T为int类型

	//a是左值,模板实例化为void Function(int& t)
	int a;
	Function(a);

	//move(a)是右值,模板实例化为void Function(int&& t)
	Function(move(a));

	//b是const左值,模板实例化为void Function(const int& t)
	const int b = 1;
	Function(b);

	//move(b)是const右值,模板实例化为void Function(const int&& t)
	Function(move(b));

	return 0;
}

运行结果:

万能引用存在的问题:

  • 以上出现的问题就是,Function函数的形参 t 无论是左值引用还是右值引用,它本身的属性就是左值,这样往Fun传参时就只会匹配左值。而我们想要达到的效果是自动匹配Fun函数,t是左值匹配左值引用的Fun,t是右值匹配右值引用的Fun。
  • 你可能会觉得move似乎能解决问题,但当我们将Fun(t)变为Fun(move(t))后,运行结果:
  • 也就是说move是不区分左值右值的,它只是强制转换为右值,所以这里并不能解决问题,我们需要的是传参给Fun时 t 依旧保持它类型的属性。
  • 这时候,完美转发就是解决这个问题的关键。

完美转发:forward

  • 完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部 t 被强转为右值引用返回;传递给 Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部 t 被强转为左值引用返回。
  • 完美转换底层依旧是强制类型转换,观察上面forward函数模板,左值匹配第一个,右值则匹配第二个。
  • forward和move一样,都是<utility>头文件中的函数,但是不同的是,forward使用时需要手动实例化:
cpp 复制代码
//万能引用
template<class T>
void Function(T&& t)
{
	//forward完美转发
	Fun(forward<T>(t));
}

运行结果:

  • 使用完美转发后,就能达到自动匹配重载函数的效果。

七、可变参数模板

  • C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包,表示零或多个函数参数。
  • 简单来说,C++11之前我们写模板是不确定类型但参数数量确定,现在有了可变参数模板,那么我们也可以在参数数量不确定的情况下去写函数模板或者类模板。
  • 因为可变参数的函数模板使用较多,我们来看其语法:
  • template<class ...Args> void Func(Args... args) {}
  • template<class ...Args> void Func(Args&... args) {}
  • template<class ...Args> void Func(Args&&... args) {}
  • 其中蓝色部分叫模板参数包,绿色部分叫函数参数包。
  • 我们用省略号来指出一个模板参数或函数参数的表示⼀个包,在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
  • 这里我们可以使用sizeof...运算符去计算参数包中参数的个数。
cpp 复制代码
#include <iostream>
using namespace std;

//可变参数模板
template<class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;
}

int main()
{
	double x = 2.2;
	Print();						//保留有0个模板参数
	Print(1);						//包里有1个模板参数
	Print(1, string("xxxxx"));		//包里有2个模板参数
	Print(1.1, string("xxxx"), x);	//包里有3个模板参数

	return 0;
}

运行结果:

1.可变参数模板的原理

  • 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
  • 比如上面演示代码中,可变参数模板本质上实例化出了以下四个函数:
cpp 复制代码
//原理1:编译本质这里会结合引用折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
  • 没有可变参数模板之前,想要达到上面的效果就得写四个函数模板:
cpp 复制代码
// 原理2:更本质去看没有可变参数模板,
// 我们实现出这样的多个函数模板才能支持这里的功能,
// 有了可变参数模板,我们进⼀步被解放,
// 他是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。
void Print();

template <class T1>
void Print(T1&& arg1);

template <class T1, class T2>
void Print(T1 && arg1, T2 && arg2);

template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
  • 现在有了可变参数模板,面对有多参数类型的函数就很方便了,换句话说,让我们泛型编程更灵活。

2.包扩展

可变参数模板虽然好用,但是我们怎么拿到具体的参数值呢,这就是包扩展该做的事

  • 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(...)来触发扩展操作。
  • 注意:我们不能把函数参数包理解为一个数组然后用[ ]去取参数,这是错误的。第一,C++还么有存储不同类型的数组。第二,就算有,模板也是编译期间解析,而[ ]这种是运行时获取和解析,所以也不支持这样使用。
  • 以下是通过递归函数拿到参数包中参数的包扩展:

第一种包扩展方式:编译期递归推导的包扩展

cpp 复制代码
#include <iostream>
using namespace std;

//递归的终止函数,当参数包是0个时匹配这个函数
void ShowList()
{
	cout << endl;
}

template<class T,class ...Args>
void ShowList(T x, Args... args)
{
	//args是N个参数的参数包
	//调用ShowList,参数包的第一个传给x,剩下的N-1个传给第二个参数包
	cout << x << " ";
	ShowList(args...);
}

//可变参数模板
template<class ...Args>
void Print(Args&&... args)
{
	ShowList(args...);//注意传参时(...)是放在参数包后面
}

int main()
{
	double x = 2.2;					
	Print(1);					
	Print(1, string("xxxxx"));		
	Print(1.1, string("xxxx"), x);

	return 0;
}

运行结果:

  • 首先需要注意的是,参数包args往其他函数传递时三个点(...)是放在参数包的后面。
  • 第二个需要注意的是,你可能会疑惑为什么要写一个ShowList()的空函数来终止递归,而不是使用 if(sizeof...(args) == 0){return;} 判断参数包个数是否为0来终止递归,我们以前终止递归都是使用if判断。首先这里比较特殊,涉及模板实例化和递归以及可变参数,我们观察下图,模板实例化是在编译期完成,所以编译期就会在最后一个函数中生成ShowList()的代码需要进行调用,但此时我们没有实现ShowList(),只实现了if判断,而if判断是在运行时进行判断,所以无法在调用ShowList()前终止函数调用,最后就是编译器认为调用不到ShowList(),存在语法错误。
  • 底层的实现细节如下图所示:
  • 这张图中就展示了编译时的递归推导过程,编译期底层就实例化出了上面四种函数。可以清晰看到右上角函数生成了ShowList()的代码需要进行调用。

**第二种包扩展方式:**直接将参数包依次展开依次作为实参给一个函数去处理。

cpp 复制代码
#include <iostream>
using namespace std;

//获取每个参数
template<class T>
const T& GetArgs(const T& x)
{
	cout << x << " ";
	return x;
}

//可变参数模板
template<class ...Args>
void Arguments(Args... args)
{
	cout << endl;
}

//可变参数模板
template<class ...Args>
void Print(Args&&... args)
{
	//利用参数的特殊性,
	//在传参前调用GetArgs将每个参数打印出来
	Arguments(GetArgs(args)...);
}

int main()
{
	double x = 2.2;
	Print(1);
	Print(1, string("xxxxx"));
	Print(1.1, string("xxxx"), x);

	return 0;
}

运行结果:

  • 初次见到这种包扩展的方式确实抽象,我们将模版实例化,以Print(1, string("xxxx"), x) 为例:
cpp 复制代码
// 本质可以理解为编译器编译时,包的扩展模式
// 将上面的函数模板扩展实例化为下面的函数
// 是不是很抽象,C++11以后,只能说委员会的大佬设计语法思维跳跃得太厉害
void Print(int x, string y, double z)
{
	Arguments(GetArgs(x), GetArgs(y), GetArgs(z));
}
  • 将模板实例化后就应该豁然开朗了,相当于在传参前,将每个参数拿出去打印再放回来一样。

3.emplace系列接口

以上的包扩展方式我们实践中也不会那样去使用,实践中的使用方式就参考emplace系列接口

  • C++11以后,所有的STL容器,只要有push_back接口的就增加了emplace_back接口,只要有insert接口的就增加了emplace接口。
  • emplace系列接口的特点:
  1. 支持万能引用,因为是函数模板的形式,所以形参的类型可以自己推导。
  2. 支持多参数传递,可变模板参数也就是参数包带来的优势。
  3. 功能上兼容push和insert系列,但是emplace还支持新玩法,假设容器为其多参数模版和万能引用的特性,可以直接在容器空间上构造T对象。
  • 下面我们来对比一下push_back和emplace_back的区别:
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <utility>
#include <list>
#include "mystring.h"

int main()
{
	list<txp::string> lt;
	txp::string s1("111111");

	//传左值,跟push_back效果一样,走拷贝构造
	lt.emplace_back(s1);
	cout << "**************************************" << endl;

	//传右值,跟push_back效果一样,走移动构造
	lt.emplace_back(move(s1));
	cout << "**************************************" << endl;

	//这里效果就和push_back不一样
	lt.emplace_back("2222222");
	cout << "**************************************" << endl;
	//对比一下运行结果就知道了
	lt.push_back("333333333");
	cout << "**************************************" << endl;

	return 0;
}

运行结果:

  • 前面的运行结果和push_back一样,最主要的区别就是最后一组:lt.emplace_back("2222222");
  • 根据上图就能看出,同样是使用字符串进行传参,emplace_back只调用了string(char* str)构造,而push_back则是先后调用了构造、移动构造、析构。push_back的调用操作我们前面都理解了。那为什么emplace_back只调用了构造呢?
  • 原因就在于emplace_back的万能引用,"222222"传给template <class... Args>void emplace_back (Args&&... args)时,因为"222222"的右值特性,Args就能推导出其 const char* 的字符串类型,从而直接调用list中的string的构造进行直接构造。
  • push_back为什么不行,"33333333"传给void push_back (value_type&& val)时,value_type早在类模板确认参数类型时就确认value_type是txp::string类型,所以const char*并不能直接构造list中的string,而是需要"3333333"先构造临时对象传给 val ,再进行移动构造,最后析构临时对象。
  • 这就是emplace系列接口万能引用的优势,当然,push_back的移动构造效率一定低很多吗,深拷贝上其实并没有什么差距,反而浅拷贝上emplace系列可以直接构造的优势明显。
  • 除此emplace还有其他优势,比如参数包支持多参数传递直接构造:

多参数传参:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <utility>
#include <list>
#include "mystring.h"

int main()
{
	list <pair<txp::string, int>> lt;
	pair<txp::string, int> kv("苹果", 1);

	//传左值,跟push_back效果一样
	lt.emplace_back(kv);
	cout << "**************************************" << endl;

	//传右值,跟push_back效果一样
	lt.emplace_back(move(kv));
	cout << "**************************************" << endl;

	//这里是多参数传参
	lt.emplace_back("苹果", 1);
	cout << "**************************************" << endl;
	//push_back无法直接传参,需要{}进行隐式类型转换
	lt.push_back({ "苹果",1 });
	//相对应的,emplace_back是不支持使用{}进行转换的
	cout << "**************************************" << endl;

	return 0;
}

运行结果:

  • 运行结果和上面一样,这次的区别在于多参数构造上:
  • push_back需要{}进行隐式类型转换,转换为pair类型再进行移动构造。
  • 而emplace_back是支持多参数传参,所以可以直接传 "苹果,"1",然后万能引用就会识别出 const char* 和 int 类型进行直接构造,和前面一样,只不多是多参数。
    总结:emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列

4.emplace系列接口模拟实现

cpp 复制代码
namespace txp
{
    //链表节点
    template<class T>
    struct ListNode
    {
        //...

        //参数包构造
        template<class ...Args>
        ListNode(Args&&... args)
            : _next(nullptr)
            , _prev(nullptr)
            , _data(forward<Args>(args)...)
        {}
    };

    //...

    //链表
    template<class T>
    class list
    {
        //...
        
        //emplace_back尾插
        template <class... Args>
        void emplace_back(Args&&... args)
        {
            emplace(end(), forward<Args>(args)...);//参数包往下传需要加...
        }

        //emplace插入
        template <class... Args>
        iterator emplace(iterator pos, Args&&... args)
        {
            Node* cur = pos._node;
            Node* newnode = new Node(forward<Args>(args)...);
            Node* prev = cur->_prev;
            // prev  newnode  cur
            prev->_next = newnode;
            newnode->_prev = prev;
            newnode->_next = cur;
            cur->_prev = newnode;
            return iterator(newnode);
        }

    private:
        Node* _head;
    };
}
  • 以上就是新增的代码,和前面实现右值版本的push_back和insert类似。
  • 首先emplace系列都是模板函数,并且是多参数模板+万能引用。
  • 那么万能引用就会涉及到一个完美转发的问题,需要保持它原本的属性,所以上面三个函数中涉及到需要往下传参的地方就需要 forward<Args> 进行完美转发。
  • 其实从实现角度看并不复杂,因为它最终还是需要调用到底层数据类型的参数包构造。

总结

以上就是本文的全部内容了,感谢支持!

相关推荐
mark-puls2 小时前
C语言打印爱心
c语言·开发语言·算法
tongsound2 小时前
igh ethercat 实时性测试
linux·c++
西阳未落2 小时前
C语言柔性数组详解与应用
c语言·开发语言·柔性数组
Huhbbjs2 小时前
SQL 核心概念与实践总结
开发语言·数据库·sql
睡不醒的kun2 小时前
leetcode算法刷题的第三十四天
数据结构·c++·算法·leetcode·职场和发展·贪心算法·动态规划
咕噜咕噜啦啦2 小时前
Qt之快捷键、事件处理、自定义按键——完成记事本项目
开发语言·qt
Source.Liu2 小时前
【Pywinauto库】12.1 pywinauto.backend 后端内部实施模块
开发语言·windows·python·自动化
晚云与城2 小时前
今日分享:C++ deque与priority_queue
开发语言·c++
半梦半醒*2 小时前
正则表达式
linux·运维·开发语言·正则表达式·centos·运维开发