目录

引言

一. C++的发展历史

二. 列表初始化

2.1 C++98传统的{}

2.2 C++11中的{}(列表初始化)

2.3 C++11中的initializer_list(初始化列表)

2.4 构造函数的成员初始化列表

关键点:

2.5 列表初始化与初始化列表的区别

三. 右值引用和移动语义

3.1 左值和右值

3.2 左值引用和右值引用

3.3 引用延长声生命周期

3.4 左值和右值的参数匹配

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

3.5.1 左值引用的使用场景

3.5.2 移动构造和移动赋值

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

1)右值对象构造,只有拷贝构造,没有移动构造的场景

2)右值对象构造,有拷贝构造,也有移动构造的场景

3)右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景

4)右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

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

3.6 类型分类

3.7 引用折叠

3.7.1 显示实例化的情况

3.7.2 不显示实例化的情况

3.8 完美转发

四. 可变参数模板

4.1 基本语法及原理

4.2 包扩展(后续新版的C++有更好的)

4.3 emplace系列接口

总结


引言

        C++11是C++语言的一个里程碑式更新,于2011年正式发布。它不仅标准化了许多既有的编程实践,还引入了众多新特性,帮助开发者编写更高效、更简洁、更安全的代码。

        本文将深入剖析 C++11 的几大核心变革,重点分析其底层实现原理及在高性能编程中的应用。通过代码示例和原理分析,帮助读者更好地理解和应用C++11。


一. C++的发展历史

C++11 是 C++ 的第二个主要版本,并且是从 C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月 12 日采纳前,人们曾使用名称“C++0x”,因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故而这是迄今为止最长的版本间隔。从那时起,C++ 有规律地每 3 年更新一次。


二. 列表初始化

2.1 C++98传统的{}

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

struct Point
{
    int _x;
    int _y;
};

int main()
{
    int array1[] = { 1, 2, 3, 4, 5 };
    int array2[5] = { 0 };
    Point p = { 1, 2 };

    return 0;
}

2.2 C++11中的{}(列表初始化)

  • C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
  • 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。 
  • {}初始化的过程中,可以省略掉=
  • C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很方便

struct Point
{
	int _x;
	int _y;
};
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;
};

C++中的列表初始化在内置类型和自定义类型中的使用:

int main()
{
    // C++98
    int array1[] = { 1,2,3,4,5 };
	int array2[5] = { 0 };
	Point p = { 1,2 };    

    Date d(2025, 1, 1);
    // C++11
    // 对于自定义类型:
    // 因为有缺省值,所以可以传一个
    Date d1 = 2025; // 构造+拷贝构造 优化-> 构造
    // 这里本质是用{ 2025, 1, 1}构造一个Date临时对象
    // 临时对象再去拷贝构造d1,编译器优化后合二为一变成{ 2025, 1, 1}直接构造初始化
    Date d2 = {2025, 12, 7}; // 多参数类型隐式类型转换
    Date d3{2025, 12, 7}; // 等号可以省略
    Date d4{}; // 默认构造
    Date d5; // 默认构造

    // 对于内置类型:
    int i = 0;
    int j = {1};
    int k{2};
    int m{}; // 0 默认构造

    return 0;
}

列表初始化的作用:1、传参;2、传返回值

void Insert(const Date& d)
{
}

Date func()
{
    // 传返回值
	//Date d(2025, 12, 7);
	//return d;
	// 可以这样写
	//return { 2025,12,7 };

	//Date d;
	//return d;
	// 可以这样写
	return {}; // 返回日期类的默认构造的对象
}

int main()
{
    // 比起有名对象和匿名对象传参,这里{}更有性价比
    Insert(2025); // 直接传参数
	Insert({ 2025, 6, 1 }); // 直接传参数
    
    vector<Date> v;
    Date d1;
    v.push_back(d1);
    v.push_back(Date(2025, 1, 1));
    // 比起有名对象和匿名对象传参,这里{}更有性价比
    v.push_back({ 2025, 1, 1 });


    return 0;
}

2.3 C++11中的initializer_list(初始化列表)

这是 C++11 引入的一个模板类,专门用来让你的类支持“列表初始化”语法。

  • 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个vector对象,我想用N个 值去构造初始化,那么我们得实现很多个构造函数才能支持, vector v1 = {1,2,3};vector v2 = {1,2,3,4,5};
  • C++11库中提出了一个std::initializer_list的类, auto il = { 10, 20, 30 }; // the type of il is an initializer_list ,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
  • std::initializer_list支持迭代器遍历。
  • 容器支持一个std::initializer_list的构造函数,也就支持任意多个值构成的 {x1,x2,x3...} 进行初始化。STL中的容器支持任意多个值构成的 {x1,x2,x3...} 进行初始化,就是通过 std::initializer_list的构造函数支持的。

int main()
{
    std::initializer_list<int> mylist;
    mylist = { 10, 20, 30 };
    cout << sizeof(mylist) << endl;

    // 这里begin和end返回的值initializer_list对象中存的两个指针
    // 这两个指针的值跟i的地址跟接近,说明数组存在栈上
    int i = 0;
    cout << mylist.begin() << endl;
    cout << mylist.end() << endl;
    cout << &i << endl;

    // {}列表中可以有任意多个值
    // 这两个写法语义上还是有差别的,第一个v1是直接构造,
    // 第二个v2是构造临时对象+临时对象拷贝v2,优化为直接构造
    vector<int> v1({ 1,2,3,4,5 });
    vector<int> v2 = { 1,2,3,4,5 };
    const vector<int>& v3 = { 1,2,3,4,5};

	// 必须是同一类型的,可以传n个
	// 这里的参数传参时是隐式类型转换为initializer_list这个类,然后用这个类构造vector
	vector<int> v1 = { 1,2,3,4,5 }; // // 调用 initializer_list 构造函数
	vector<int> v2{ 2,4,5,66,7,3,2,11,4 };

	// 也可以赋值
	v1 = { 10,20,30 }; // 隐式类型转换;构造+拷贝构造 优化-> 构造

	// 外层是pair类型的初始化列表,内层是多参数的隐式类型转换
	map<string, string> dict{ {"sort", "排序"},{"string","字符串"} };

    // 直接使用,编译器识别出来的类型是std::initializer_list
	auto il = { 1,2,3 };
	cout << typeid(il).name() << endl;

	return 0;
}

注意:由于initializer_list是一个模板类,只能实例化出一种类型,所以initializer_list只能存储同一种类型的数据。

#include <initializer_list>
#include <vector>

class MyVector {
    std::vector<int> data;
public:
    // 有了这个构造函数,就能用 {} 初始化
    MyVector(std::initializer_list<int> list) {
        data.assign(list.begin(), list.end());
    }

    // 也可以提供普通构造函数
    MyVector(int size, int value = 0) : data(size, value) {}
};

// 使用
MyVector v1{1, 2, 3, 4, 5};     // 调用 initializer_list 构造函数
MyVector v2 = {1, 2, 3, 4, 5};  // 同上
MyVector v3(10, 0);             // 调用普通构造函数

注意:如果类既有普通构造函数,又有 std::initializer_list 构造函数,用 {} 时永远优先调用 initializer_list 的!

struct Magic {
    Magic(int, double);           // 普通构造函数
    Magic(std::initializer_list<int>);  // initializer_list 构造函数
};

Magic m{1, 3.14};   // 本意想调用 (int,double),但实际调用 initializer_list<int>!编译错误
Magic m2(1, 3.14);  // 正确调用 (int,double)

2.4 构造函数的成员初始化列表

这是完全不同的语法,用在构造函数定义时,用冒号 : 后面初始化类的数据成员。它不是C++11新引入的,注意不要与初始化列表模板类搞混

class Example {
private:
    const int id;
    std::string& ref;
    std::vector<int> vec;

public:
    // 这就是“初始化列表”(constructor initializer list)
    Example(int i, std::string& s, std::vector<int> v)
        : id(i),              // const 成员必须在这里初始化
          ref(s),             // 引用必须在这里初始化
          vec(std::move(v))   // 推荐用移动
    {
        // 构造函数体
        std::cout << "constructed" << std::endl;
    }
};
关键点:
  • 必须用于初始化 const 成员、引用成员、无默认构造函数的成员
  • 初始化顺序是类中成员声明的顺序,而不是初始化列表里写的顺序!
  • 在进入构造函数体之前就完成了初始化,效率更高(尤其是避免了成员对象的默认构造+赋值)

2.5 列表初始化与初始化列表的区别

名称 语法形式 出现位置 主要用途 典型例子
列表初始化(Uniform Initialization) {} 初始化对象 任何需要初始化的地方 统一、安全的初始化方式,防止窄化转换 int x{5}; Vector v{1,2,3};
初始化列表(std::initializer_list) 参数类型是 std::initializer_list<T> 构造函数或普通函数参数 让你的类型支持 {1,2,3} 这种字面量初始化 vector<int>{1,2,3} 调用它的构造函数
构造函数初始化列表(Constructor Initializer List) 构造函数后 : member(value) 只有构造函数定义里 初始化成员(尤其是 const、引用、基类) : id(5), name("aa") {}

三. 右值引用和移动语义

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


3.1 左值和右值

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

int main()
{	
	// 左值:可以取地址
	// 以下的p、b、c、*p、s、s[0]就是常见的左值
	int* p = new int(0);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("111111");
	s[0] = 'x'; // s[0]是数据表达式

	cout << &p << endl; // 在栈上
	cout << &c << endl;
	cout << &(*p) << endl; // 在堆上
	cout << &s << endl;
	//cout << &s[0] << endl; // 这个取地址类型是const char*,会带引字符串,而不是打印地址
	cout << (void*)&s[0] << endl; // 要强转为void*。因为string有一个小数组,所以字符串比较短的话在栈上,稍微长点就在堆上了

	// 右值:不能取地址
	double x = 1.1, y = 2.2;
	// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
	10;
	x + y; // 相加的结果会存在寄存器中
	fmin(x, y); // 这里的右值是传值返回产生的临时对象
	string("11111"); // 匿名对象

	// 不能取地址,因为可能就没有地址
	//cout << &10 << endl;
	//cout << &(x+y) << endl;
	//cout << &(fmin(x, y)) << endl;
	//cout << &string("11111") << endl;

	return 0;
}

3.2 左值引用和右值引用

  • Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
  • 左值引用不能直接引用右值,但是const左值引用可以引用右值 
  • 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
  • move是库里面的一个函数模板:template typename remove_reference::type&& move (T&& arg); 本质内部是进行强制类型转换
  • 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量属性是左值
  • 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中r1和rr1 汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的。

int main()
{
	// 左值:可以取地址
	// 以下的p、b、c、*p、s、s[0]就是常见的左值
	int* p = new int(0);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("111111");
	s[0] = 'x';
	double x = 1.1, y = 2.2;

	// 左值引用给左值取别名
	int& r1 = b;
	int*& r2 = p;
	int& r3 = *p;
	string& r4 = s;
	char& r5 = s[0];
	// 右值引用给右值取别名
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("11111");

	// 左值引用不能直接引用右值,但是const左值引用可以引用右值
	const int& rx1 = 10;
	const double& rx2 = x + y;
	const double& rx3 = fmin(x, y);
	const string& rx4 = string("11111");
	// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
	int&& rrx1 = move(b);
	int*&& rrx2 = move(p);
	int&& rrx3 = move(*p);
	string&& rrx4 = move(s);
	// move的底层本质相当于强转
	string&& rrx5 = (string&&)s;

	// rrx1:右值引用变量表达式的属性是左值,不能直接被右值引用绑定
	//int&& rrr1 = rrx1;
    // move一下就可以了
	int&& rrr1 = move(rrx1);

	return 0;
}

3.3 引用延长声生命周期

右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生存期,但const 的左值引用对象无法被修改。

int main()
{
	// 延长生命周期
	string s1 = "test";
	//string&& r1 = s1;

	// 左值引用+const或右值引用都可以延长右值的生命周期
	const string& r2 = s1 + s1;
	string&& r3 = s1 + s1;

	// 左值引用+const,不能修改
	//r2 += "test";
	// 右值引用不加const,可以修改;因为右值引用本质属性是左值
	r3 += "test";

	return 0;
}

3.4 左值和右值的参数匹配

  • C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
  • C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。
  • 右值引用变量在用于表达式时属性是左值,这样的设计对后续使用有很重要的作用

void f(int& x)
{
	std::cout << "左值引用重载 f(" << x << ")\n";
}

void f(const int& x) // 用const左值引用,既可以传左值也可以传右值
{
	std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}

void f(int&& x)
{
	std::cout << "右值引用重载 f(" << x << ")\n";
}

int main()
{
	// 找最匹配的
	f(10); // 右值引用

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

	int a = 20;
	f(a); // 左值引用

	const int b = 30;
	f(b); // const左值引用

	return 0;
}

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

3.5.1 左值引用的使用场景

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如addStrings和generate函数,C++98中的解决方案只能是被迫使用输出型参数解决。那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。

class Solution {
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;
    }
};

左值引用的主要使用场景:

  • 传左值引用传参,减少拷贝
  • 形参的改变可以影响实参
  • 传左值引用返回,减少拷贝(部分场景可用:如果返回对象是临时对象,那么无法传左值引用返回)
  • 修改返回对象

3.5.2 移动构造和移动赋值

移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引 用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值

移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函 数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用

对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有 意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要“窃取”引用的 右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的My::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。


namespace My {
	class string
	{
	public:
        // 构造
		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)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl; reserve(s._capacity);
			for (auto ch : s)
			{
				push_back(ch);
			}
		}
		// 移动构造(右值拷贝)
		string(string&& s)
		{
			// 右值引用(s)的属性是左值,才能对右值进行修改,这样才能将右值的资源转移出来(通过调用swap,swap的参数是左值引用)
			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)
		{
			// 右值引用(s)的属性是左值,这样才能对右值进行修改,这样才能将右值的资源转移出来(通过调用swap,swap的参数是左值引用)
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
		~string()
		{
			//cout << "~string() -- 析构" << endl;
			delete[] _str;
			_str = nullptr;
		}

	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 << &str << endl;
		return str;
		// 按语义理解str是左值,因为是局部对象
		// 传值返回返回的对象,会被识别为右值;可以理解为,它被编译器特殊处理,move(强转)了一下
		// 从意义上来说,把它识别为右值是没有问题的
	}
}

// 拷贝构造和移动构造
int main()
{
  // 传参时会优化为构造
	My::string ret = bit::addStrings("111", "222");
	// 这里编译器作了优化,str是ret的别名,str改变就会影响ret(str和ret地址相同)
	// 没有拷贝构造或移动构造
	cout << &ret << endl;

	return 0;
}
// 拷贝赋值和移动赋值
int main()
{
	My::string ret;
	// ...
	// 赋值
	ret = bit::addStrings("123", "678"); // 编译器优化为:把str直接赋值给ret
	// 这里即使编译器优化了也会有拷贝赋值/移动赋值,而这里运用移动赋值就会提高效率

	cout << &ret << endl;

	return 0;
}

 VS2019 release 及以后、g++9 release 及以后,传值返回已经没有拷贝,是否意味着右值引用和移动语义就没有意义?        不是
 1、没有拷贝构造目前不是标准规定
 2、只有比较新的编译器才会做这样的优化
 3、其他场景做不到这样的优化(不拷贝)


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

namespace bit
{
	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;
	}
}

// 场景1
int main()
{
	bit::string ret = bit::addStrings("11111", "2222");
	cout << ret.c_str() << endl;
	return 0;
}
// 场景2
int main()
{
	bit::string ret;
	ret = bit::addStrings("11111", "2222");
	cout << ret.c_str() << endl;
	return 0;
}

1)右值对象构造,只有拷贝构造,没有移动构造的场景
  • 图1展示了vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右 边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
  • 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接 将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。 变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图3所示。
  • linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elideconstructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次拷贝


2)右值对象构造,有拷贝构造,也有移动构造的场景
  • 图2展示了vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。
  • 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。 要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图3所示。
  • linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elideconstructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次移动。



3)右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
  • 图4左边展示了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境 下编译器的处理,一次拷贝构造,一次拷贝赋值。
  • 需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名


4)右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
  • 图5左边展示了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境 下编译器的处理,一次移动构造,一次移动赋值。
  • 需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造 要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以 看到str的析构是在赋值以后,说明str就是临时对象的别名


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

  • 查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本
  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
  • 当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上
  • 把我们模拟实现的My::list拷贝过来,支持右值引用参数版本的push_back和insert
// void push_back (const value_type& val);
// void push_back (value_type&& val);
// iterator insert (const_iterator position, value_type&& val);
// iterator insert (const_iterator position, const value_type& val);

int main()
{
	List::list<bit::string> lt;
	cout << "*************************" << endl;

	bit::string s1("111111111111111111111");

	lt.push_back(s1); // s1,左值,调用拷贝构造
	cout << "*************************" << endl;

	lt.push_back(bit::string("22222222222222222222222222222")); // 匿名对象,右值,移动构造
	cout << "*************************" << endl;

	// 推荐这种写法:1、方便; 2、效率高(移动构造)
	// 单参数就像下面这样写,多参数就用花括号,都是隐式类型转换
	lt.push_back("3333333333333333333333333333"); // 隐式类型转换,右值,移动构造
	cout << "*************************" << endl;

	// 左值move本质授予了别人转移你资源的权限,所以要谨慎,move之后就不能用了
	lt.push_back(move(s1)); // 右值,移动构造
	cout << "*************************" << endl;
	return 0;
}

3.6 类型分类

  • C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值 (expiring value,简称xvalue)。
  • 纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如: 42、 true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整 形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于 C++98中的右值。
  • 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如 move(x)、static_cast(x)
  • 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。


3.7 引用折叠

  • C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或 typedef 中的类型操作可以构成引用的引用。
  • 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规 则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。 
  • 下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则,大家需要一个一 个仔细理解一下。
  • 像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左 值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
  • Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的 Function,实参是右值,实例化出右值引用版本形参的Function

3.7.1 显示实例化的情况

// 由于引用折叠限定,f1实例化以后总是一个左值引用
template<class T>
void f1(T& x)
{
}
// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{
}

int main()
{
    // 不能直接定义引用的引用
    //int&&& r = i;

    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&&

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

    // 没有折叠->实例化为void f2(int&& x)
    f2<int>(n); // 报错
    f2<int>(0);
    // 折叠->实例化为void f2(int& x)
    f2<int&>(n);
    f2<int&>(0); // 报错
    // 折叠->实例化为void f2(int&& x)
    f2<int&&>(n); // 报错
    f2<int&&>(0);
    // 折叠->实例化为void f2(const int& x)
    f2<const int&>(n);
    f2<const int&>(0);
    // 折叠->实例化为void f2(const int&& x) (一般右值引用不加const,因为右值引用是左值属性,我们就是期望它能够被修改)
    f2<const int&&>(n); // 报错
    f2<const int&&>(0);

	return 0;
}

3.7.2 不显示实例化的情况

// 在模板里面看到这样的写法,不能叫右值引用,而是叫做万能引用(既可能是左值引用也可能是右值引用)
// 传左值,实例化出左值引用的Function函数,传右值,实例化出右值引用的Function函数(如有const也会带上)
// 这就是引用折叠设计的意义
template<class T>
void Function(T&& t)
{
    int a = 0;
    T x = a;
    //x++;
    // 看a和x地址是否相同可以证明T的类型
    cout << &a << endl;
    cout << &x << endl << endl;
}
int main()
{
    // 10是右值,推导出T为int,模板实例化为void Function(int&& t)
    Function(10); // 右值
   
    int a;
    // a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
    Function(a); // 左值
    
    // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
    Function(std::move(a)); // 右值
   
    const int b = 8;
    // b是const左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
    // 所以Function内部会编译报错,x不能++
    Function(b);
    
    // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
    // 所以Function内部会编译报错,x不能++
    Function(std::move(b)); // const 右值
   
    return 0;
}

3.8 完美转发

  • Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化 以后是右值引用的Function函数。
  • 变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性, 就需要使用完美转发实现。
  • template T&& forward (typename remove_reference::type& arg);
  • template T&& forward (typename remove_reference::type&& arg);
  • 完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给 Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给 Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用 返回。

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); // 如果不用完美转发,无论传的是左值还是右值,这里的t都是左值属性
    // 完美转发在万能引用中使用
    Fun(forward<T>(t)); // 保持它的属性不变,否则如果是右值,但变成了左值属性,再往下传递就无法使用移动构造了
}

int main()
{
    // 10是右值,推导出T为int,模板实例化为void Function(int&& t)
    Function(10); // 右值
    
    int a;
    // a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
    Function(a); // 左值
    
    // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
    Function(std::move(a)); // 右值
    
    const int b = 8;
    // b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&t)
    Function(b); // const 左值
    
    // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
    Function(std::move(b)); // const 右值

    return 0;
}

四. 可变参数模板

4.1 基本语法及原理

  • C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
  • template void Func(Args... args) {} 
  • template void Func(Args&... args) {} 
  • template void Func(Args&&... args) {} 
  • 我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出 接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板 一样,每个参数实例化时遵循引用折叠规则。 
  • 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
  • 这里我们可以使用sizeof...运算符去计算参数包中参数的个数。

// 之前是类型可变,现在是类型可变,个数也可变
template <class ...Args>
void Print(Args&&... args)
{
    // sizeof...才能对这个参数包sizeof,看这个形参有几个
    cout << sizeof...(args) << endl;
}
 
// 可变参数模板的原理跟模板类似,编译器底层原理还是一样的,本质还是去实例化对应类型和个数的多个函数
// main函数中调用时,编译器实例化出的对应函数如下:
void Print()
{
    cout << 0 << endl;
}
void Print(int&& x)
{
    // sizeof在编译时计算的
    cout << 1 << endl;
}
void Print(double& d, string&& s)
{
    cout << 2 << endl;
}

int main()
{
    double x = 2.2;
    Print(); // 包里有0个参数
    Print(1); // 包里有1个参数
    Print(x, string("xxxxx")); // 包里有2个参数
    Print(1.1, string("xxxxx"), x); // 包里有3个参数
    
    return 0;
}

底层原理:

// 原理1:编译本质这里会结合引用折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

// 原理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);
// ...

4.2 包扩展(后续新版的C++有更好的)

  • 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个 包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元 素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(...)来触发扩展操作。底层 的实现细节如图1所示。
  • C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。


//template <class ...Args>
//void Print(Args&&... args)
//{
//    // 错误写法
//    // 可变参数模板编译时解析
//    // 下面是运行获取和解析,所以不支持这样用
//    for (size_t i = 0; i < sizeof...(args); i++)
//    {
//        cout << args[i] << " ";
//    }
//    cout << endl;
//}

// 递归终止条件(处理空包或最后一个参数后的情况)
void ShowList()
{
    // 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
    cout << endl;
}

template <class T, class ...Args>
void ShowList(T x, Args... args)
{
    cout << x << " ";

    // 能否这样结束递归? 不能
    // 因为这是运行时的递归逻辑,而递归的每个模板在编译时就实例化了,真正的递归展开是在编译期完成的
    //if (sizeof...(args) == 0)
    //    return;

    // args是N个参数的参数包
    // 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
    ShowList(args...);
}

// 打印参数包内容
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
    // 解析参数包,递归时的实例化推导
    ShowList(args...);
}

int main()
{
    Print();
    Print(1);
    Print(1, string("xxxxx"));
    Print(1, string("xxxxx"), 2.2);
    return 0;
}

C++17版本的折叠表达式(后续再讲解)

// 由于上面的包拓展比较复杂,所以后来C++17又发明了折叠表达式
// 折叠表达式(C++17)
template <class ...Args>
void Print(Args... args)
{
    // 逗号折叠表达式(也有其他折叠,暂不学)
    ((cout << args << " "), ...);

    //int ret = 0;
    //((ret += args), ...); // 每个值都把他加等上去

    cout << endl;
}

// 原理展示:折叠是写的时候,展开是编译的时候
// Print(1, string("xxxxx"), 2.2);
// 实例化为下面的函数
void Print(int x, string y, double z)
{
    //((cout << args << " "), ...);
    ((cout << x << " "), (cout << y << " "), (cout << z << " "));

    cout << "\n";
}

4.3 emplace系列接口

  • template void emplace_back (Args&&... args);
  • template iterator emplace (const_iterator position, Args&&... args); 
  • C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。 
  • emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列 
  • 第二个程序中我们模拟实现了list的emplace和emplace_back接口,这里把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面说的empalce支持 直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。 
  • 传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下 std::forward(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值

// emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列
int main()
{
    list<bit::string> lt;

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

    // 右值,跟push_back一样,走移动构造
    lt.emplace_back(move(s1));
    cout << "*********************************" << endl;
    
    // 直接把构造string参数包往下传,直接用string参数包构造string
    // 这里达到的效果是push_back做不到的;这里效率差别不是很大
    lt.emplace_back("111111111111"); // 构造  emplace_back是函数模板,是在传参时实例化的
    lt.push_back("111111111111"); // 构造+移动构造    不是模板,还会构造临时对象;这里的参数在创建对象时(传参前)就已经实例化好了
    cout << "*********************************" << endl;
    
    return 0;
}

struct Date
{
    int _y;
    int _m;
    int _d;

    Date(int y = 1, int m = 1, int d = 1)
        :_y(y)
        , _m(m)
        , _d(d)
    {
    }
};

// 分析库里面的push_back和emplace_back
int main()
{
    list<pair<bit::string, int>> lt1;

    // 跟push_back一样
    // 构造pair + 拷贝/移动构造pair到list的节点中data上
    pair<bit::string, int> kv("苹果", 1);
    lt1.emplace_back(kv);
    cout << "*********************************" << endl;

    // 跟push_back一样
    lt1.emplace_back(move(kv));
    cout << "*********************************" << endl;

    // 直接把构造pair参数包往下传,直接用pair参数包构造pair
    // 这里达到的效果是push_back做不到的
    lt1.emplace_back("苹果", 1);
    //lt1.emplace_back({ "苹果", 1 }); // err:emplace_back 接收的是“构造参数”,而不是“初始化列表”;{} 不能参与模板参数推导,因此不能作为 emplace_back 的实参。
    //lt1.push_back("苹果", 1); // 错误,要传pair或者{}隐式转换pair的值
    //lt1.push_back({"苹果", 1}); // 要传pair或者{}隐式转换pair的值
    cout << "*********************************" << endl;

    list<Date> lt;
    // 构造 + 拷贝构造
    Date d1{ 2025,11,18 };
    lt.push_back(d1);
    lt.push_back({ 2025,11,18 });

    // 传构造Date的参数,传给形参参数包,参数包往下不断传递,最后直接构造到链表节点上
    // 直接构造
    lt.emplace_back(2025,11,18);

    return 0;
}

总结

如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐