别再只懂 C++98!C++11 这7个核心特性,直接拉开你与普通开发者的差距

🎬 个人主页Vect个人主页

🎬 GitHubVect的代码仓库
🔥 个人专栏 : 《数据结构与算法》《C++学习之旅》《计算机基础

⛺️Per aspera ad astra.



文章目录

  • [1. 初始化方式](#1. 初始化方式)
    • [1.1. `{}`初始化](#1.1. {}初始化)
    • [1.2. `std::initializer_list`](#1.2. std::initializer_list)
  • [2. 声明方式](#2. 声明方式)
    • [2.1. `auto`](#2.1. auto)
    • [2.2. `decltype`](#2.2. decltype)
  • [3. 右值引用和移动语义](#3. 右值引用和移动语义)
  • 4.完美转发的应用------容器的`emplace`系列
    • [4.1. **核心意义:原地构造+完美转发**](#4.1. 核心意义:原地构造+完美转发)
    • [4.2. 使用场景](#4.2. 使用场景)
      • [4.2.1. 场景一:`vector::emplace_back`VS`push_back`](#4.2.1. 场景一:vector::emplace_backVSpush_back)
      • [4.2.2. 场景二:`map::emplace`VS `insert`](#4.2.2. 场景二:map::emplaceVS insert)
    • [4.3. 底层机制](#4.3. 底层机制)
  • [5. 可变参数模板](#5. 可变参数模板)
  • [6. `lambda`表达式](#6. lambda表达式)
    • [6.1. 基本语法和用法](#6.1. 基本语法和用法)
    • [6.2. 底层原理](#6.2. 底层原理)
    • [6.3. 捕获](#6.3. 捕获)
      • [6.3.1. 按值捕获`[=]/[x]`](#6.3.1. 按值捕获[=]/[x])
      • [6.3.2. 按引用捕获`[&]/[&X]`](#6.3.2. 按引用捕获[&]/[&X])
      • [6.3.3. 混合捕获](#6.3.3. 混合捕获)
    • [6.4. `mutable` 和`const`](#6.4. mutableconst)
    • [6.5. `lambda`和函数对象](#6.5. lambda和函数对象)
  • [7. 包装器](#7. 包装器)
    • [7.0. 什么是可调用对象?](#7.0. 什么是可调用对象?)
    • [7.1. 模板函数的问题](#7.1. 模板函数的问题)
    • [7.2. 包装器](#7.2. 包装器)
    • [7.3. 用包装器改造`useF`](#7.3. 用包装器改造useF)
    • [7.4. 迷你包装器实现](#7.4. 迷你包装器实现)

1. 初始化方式

1.1. {}初始化

在C++98中,{}对数组或结构体元素进行统一的列表初始化,例如:

cpp 复制代码
// C++98 用{}对数组和结构体进行统一的列表初始化
int arr[] = { 10,20,34,2,2 };
struct showEG {
	double x;
	double y;
	double z;
};
struct showEG pos = { 0.1,0.8 ,1.8};

C++11扩大了{}适用范围,适用于所有的内置类型和自定义类型,使用初始化列表时,也可以省略=

cpp 复制代码
// C++11 对所有内置类型和自定义类型 {}进行初始化 =可以省略
struct Point {
	double x;
	double y;
	double z;
};
struct Point pos{1.1,1.1,1.1};

int num{ 10 };
long long len{ 1024 };

int arr[]{ 1,2,3,4,6,7 };

创建对象时也可以采用这种方式:

cpp 复制代码
class Date {
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
public:
	Date(int year,int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{ }
};
// 之前的初始化方式
Date d(2020, 1, 1);
// C++11引入的初始化方式
Date d1{ 2025,11,11 };

1.2. std::initializer_list

std::initializer_list{} 这个"初始化列表"成为一种正式的类型,从而:

  1. 你可以写 vector<int> v = {1,2,3};
  2. 甚至可以写 v = {10,20,30};
  3. 自己的类也能像 STL 一样支持 {} 初始化/赋值。

这是std::initializer_list的类型

它的底层是一个数组,当写到:

cpp 复制代码
std::vector<int> = {1,2,3};

编译器会有下列行为:

  1. 在某个连续内存中放好{1,2,3}这个只读数组
  2. 构造一个 std::initializer_list<int>对象il
    • il.begin():指向临时数组的首元素
    • il.end():指向临时数组最后元素的下一个
  3. 调用vector(initializer_list<int> il)构造函数

以下是std::initializer_list的使用:

cpp 复制代码
// 要和Date d1{ 2025,11,11 }区分开
// {}里的元素个数取决于Date类中成员变量个数

// 而以下{}里元素个数不限

vector<int> v = { 1,2,3,4 };

map<string, string> dict = { {"书","book"},{"排序","sort"} };

2. 声明方式

2.1. auto

C++11中,auto用于实现类型的自动推断,这就要求必须显式初始化,让编译器将定义的对象类型设置为初始化值的类型

cpp 复制代码
void showAuto() {
	auto num = 10.4;
	cout << typeid(num).name() << endl << endl;

	map<int, string> m = { {10,"hh"}, {12,"aa"} };
	cout << typeid(m).name() << endl;

}

2.2. decltype

将类型声明为表达式指定的类型,并计算表达式的结果

cpp 复制代码
template<class T1, class T2>
void Func(T1 num1, T2 num2) {
	decltype(num1 * num2) ret;
	cout << typeid(ret).name() << endl;
}

void showDecltype() {
	const int x = 1;
	double y = 2.6;

	decltype(x * y) ret;
	decltype(&x) pos;
	cout << typeid(ret).name() << endl;
	cout << typeid(pos).name() << endl;

	Func('a', 1.5);
}

3. 右值引用和移动语义

3.1. 左值引用和右值引用

不论是左值引用还是右值引用,都是取别名

什么是左值?什么是左值引用?

左值:可以获取地址 的表示数据的表达式,可以对左值赋值,左值可以出现在=两边 ,而定义时有const修饰后的左值,不能给他赋值,但可以取地址

cpp 复制代码
void showLeftValue() {
	// num dnum *pnum 都是左值
	int num = 10;
	double dnum = 1.5;
	const int* pnum = &num;

	// rnum rdnum rpnum 都是左值引用
	int& rnum = num;			// 引用int类型变量
	double& rdnum = dnum;		// 引用double类型变量
	const int*& rpnum = pnum;	// 引用const指针类型变量

	// 可以给左值赋值
	num = 20;
	// 左值可以出现在=右侧
	dnum = (double)num;
}

什么是右值?什么是右值引用?

右值:不能获取地址 的表示数据的表达式,通常有字面常量,表达式返回值,函数返回值 ,右值只能出现在=的右侧。

cpp 复制代码
void showRightValue() {
	// "13245" 1.1 2.2 Fmin('a', 'c') (x + y)都是右值
	string s = "13245";
	double x = 1.1, y = 2.2;
	Fmin('a', 'c');
	double plus = x + y;

	// 以下都是右值引用
	int&& rrnum = 10;
	double&& rrplus = x + y;
	char&& ret = Fmin('a', 'c');

	// 右值不能出现在=左边
	//  error C2106: "=": 左操作数必须为左值
	//10 = x;
	//x + y = 2.0;
	//Fmin('a', 'b') = 0;

}

这里需要注意,不能给右值取地址,但是引用右值之后,会把右值存到特定位置,可以取到这个地址,也就是说不能取字面常量10的地址,但是rrnum饮用后,可以对rrnum取地址,也可以修改rrnum

我们转到汇编层,会发现并没有引用的概念,都是有详细的地址的

3.2. 左值引用和右值引用的区别

左值引用:

  • 左值引用只能引用左值,不能引用右值
  • const左值引用既可以引用左值,也能引用右值-->右值有临时属性,相当于是临时变量,而临时变量有常性,只读
cpp 复制代码
// 左值引用只能引用左值
int val = 10;
int& rval1 = val;
// C2440: "初始化": 无法从"int"转换为"int &" 非常量引用只能绑定到左值
//int& rval2 = 10;

// const左值引用可以引用右值
const int&& rrval = 10;

右值引用:

  • 右值引用只能引用右值,不能引用左值
  • 右值引用可以引用move后的值
cpp 复制代码
// 右值引用只能引用右值
double num = 1.1;
double&& rrnum1 = 1.1;
// C2440: "初始化": 无法从"double"转换为"double &&" 无法将左值绑定到右值引用
//double&& rrnum2 = num;

// 右值引用可以引用move后的左值
double&& rrnum = move(num);

需要注意的是move并不改变num的左值属性,经过move(num)的返回值类型是右值

3.3. 右值引用的使用场景

3.3.1. 用一个buffer观察返回值的拷贝和移动

写一个简单的动态数组类:

cpp 复制代码
class buffer {
public:
	// 普通构造 开一块空间
	buffer(size_t n)
		:_data(new int[n])
		, _size(n) 
	{
		cout << "构造buffer(" << n << ")" << endl;
	}

	// 拷贝构造 深拷贝
	buffer(const buffer& other)
		:_data(new int[other._size])
		, _size(other._size)
	{
		cout << "拷贝构造 buffer" << endl;
		for (size_t i = 0; i < _size; i++)
		{
			_data[i] = other._data[i];
		}
	}
	// 析构
	~buffer() {
		cout << "析构buffer" << endl;
		delete[] _data;
	}
private:
	int* _data;
	size_t _size;

};

1. 没有移动构造:函数返回会发生什么?

没有移动构造的情况下,大致流程:

  1. MakeBuffer() 里构造 buf → 调用普通构造
  2. return buf; 时:
    • 需要把 buf 的内容拷贝给"返回值临时对象" → 调用拷贝构造
  3. mainBuffer b = MakeBuffer();
    • 再把"返回值临时对象"拷贝给 b → 又一次拷贝构造
  4. 最后局部对象/临时对象依次析构 → 对应多次"释放内存"

也就是说,可能会发生 2 次深拷贝(新申请内存 + 复制数据)。

2. 加上移动构造之后的现象:

移动构造的本质是讲参数右值的资源窃取过来,占为己有,就不用深拷贝了

现在添加一个移动构造函数:

cpp 复制代码
// 移动构造 偷资源
// other的资源即将释放 this拿来直接用
buffer(buffer&& other) noexcept
	:_data(other._data)
	, _size(other._size)
{
	cout << "移动构造 buffer" << endl;
	other._data = nullptr;
	other._size = 0;
}
	

但是我们发现,只有一次构造,这是编译器优化的太厉害了:

编译器发现我的逻辑是先构造一个局部对象,再拷贝/移动一份给外面的人用

而编译器说:我干嘛要先构造再搬?

我直接再外面那块内存上"就地构造"不就完了

这是编译器的行为:

3. 总结一下:

我们现在遇到两个现象:

  • return buf:只打印"构造buffer(10)"和析构buffer,没有看到移动构造
  • return (move)buf:VS2026给出警告,一次构造,一次移动,两次析构

先搞清楚三个概念:拷贝、移动、拷贝消除(RVO/NRVO)

  • 拷贝构造 :开辟新空间,把别人的数据一份一份拷贝过来(深拷贝)
  • 移动构造 : 直接偷指针,把别人的_data指针接管过来,把对方置空
  • 拷贝消除 : 省略掉拷贝/移动,直接在对应空间就地构造

所以,对于return buf;buffer b = MakeBuffer();,既然buf最终要传给b,那么干脆在b的空间上直接构造buf,这是常见的RVO(Return Value Optimization)

而对于返回的是一个函数内的局部自动对象,类型与返回值相同,就符合NRVO(Named RVO)

而对于return (move)buf,表达式变为一个将亡值,不再是一个局部变量的名字,不符合NRVO,语义变为:

  1. buf 调用移动构造,构造一个"返回值临时对象 rv"

  2. buf 资源被搬到 rv 上

  3. 退出函数,把这个 rv 返回给 main

  4. main 里用 rv 再移动构造 b

而VS这里的警告是:用了move会限制优化

3.4. 升级版buffer+日志再次理解右值引用

添加移动赋值和拷贝赋值

cpp 复制代码
class buffer {
public:
	// 普通构造 开一块空间
	buffer(size_t n)
		:_data(new int[n])
		, _size(n) 
	{
		cout << "构造buffer(" << n << ")" << endl;
	}

	// 拷贝构造 深拷贝
	buffer(const buffer& other)
		:_data(new int[other._size])
		, _size(other._size)
	{
		cout << "拷贝构造 buffer" << endl;
		for (size_t i = 0; i < _size; i++)
		{
			_data[i] = other._data[i];
		}
	}

	// 移动构造 偷资源
	// other的资源即将释放 this拿来直接用
	buffer(buffer&& other) noexcept
		:_data(other._data)
		, _size(other._size)
	{
		cout << "移动构造 buffer" << endl;
		other._data = nullptr;
		other._size = 0;
	}
	
	// 拷贝赋值:深拷贝+释放旧资源
	buffer& operator=(const buffer& other) {
		cout << "拷贝赋值 buffer" << endl;
		if (this != &other) {
			// 先备份旧数据 避免自拷贝
			int* newData = other._size ? new int[other._size] : nullptr;
			for (size_t i = 0; i < other._size; i++)
			{
				newData[i] = other._data[i];
			}
			delete[] _data;
			_data = newData;
			_size = other._size;
		}
		return *this;
	}

	// 移动赋值
	buffer& operator=(buffer&& other) noexcept {
		cout << "移动赋值 buffer" << endl;
		if (this != &other) {
			delete[] _data;	// 先释放旧资源

			_data = other._data; // 直接接管对方资源
			_size = other._size;
			other._data = nullptr;
			other._size = 0;
		}
		return *this;
	}

	// 析构
	~buffer() {
		cout << "析构buffer" << endl;
		delete[] _data;
	}

	size_t size() const { return _size; }
private:
	int* _data;
	size_t _size;

};

void testNoExpand() {
	cout << "=== 不扩容场景: push_back 左值/右值 ===" << endl;
	vector<buffer> v;
	v.reserve(3);

	cout << "\n---插入左值---\n";
	buffer b1(10);
	v.push_back(b1);

	cout << "\n---插入右值---\n";
	v.push_back(buffer(20));

	cout << "\n---插入move(b1)---\n";
	v.push_back(move(b1));

	cout << "=== 析构 ===" << endl;
}

int main() {
	testNoExpand();

	return 0;

分析一下详细过程:

  • v.push_back(b1); ------ 左值插入

    1. buffer b1(10);

      • 调用普通构造:构造 buffer(10)
    2. v.push_back(b1);

      • b1 是左值,匹配 push_back(const T&) 版本

      • vector 在它预留的空间里原地构造一个元素 :调用 拷贝构造

      • 输出:拷贝构造 buffer

    3. 此时:

      • b1 拥有一块 size=10 的数组

      • v[0] 也有一块 size=10 的数组,独立深拷贝

  • v.push_back(buffer(20)); ------ 使用右值插入

    1. buffer(20) 先在语句求值时生成一个临时对象:

      • 输出:构造 buffer(20)
    2. push_back 接收到的是一个右值,匹配 push_back(T&&) 版本

      • vector 在自己的存储中构造元素时 优先调用移动构造

      • 输出:移动构造 buffer

    3. 那个临时 buffer(20) 用完后会析构:

      • 输出:析构 buffer(因为资源已经被偷走,_size 已变 0)
  • v.push_back(std::move(b1)); ------ 把已有对象资源挪进去

    1. std::move(b1)b1 从左值修饰成右值

    2. 匹配 push_back(T&&),内部调用 移动构造

      • 输出:移动构造 buffer

      • 新元素接管 b1 的 _data/_size

      • b1 变成 _data=nullptr, _size=0

    3. 最后 main 结束:

      • 依次析构 v[2]、v[1]、v[0]、b1

      • b1 已经是空壳,所以不会析构两次

结论:

  • 传左值 → 只能拷贝构造(深拷贝 O(n)
  • 传右值 / std::move → 优先移动构造(偷指针 O(1)

再来看下扩容版本:

cpp 复制代码
void testExpand() {
	cout << "=== 扩容场景: vector 重新分配 ===" << endl;
	vector<buffer> v;
	v.reserve(2);

	cout << "\n push_back(b1) \n";
	buffer b1(10);
	v.push_back(b1);

	cout << "\n push_back(b1) \n";
	buffer b2(20);
	v.push_back(b2);

	cout << "\n 第三次 触发扩容机制 \n";
	buffer b3(30);
	v.push_back(b3);

	cout << "=== 析构 ===" << endl;
}

v 从容量 2 增长到 3 时,典型流程是:

  1. vector 申请一块更大的新空间(例如容量变成 4)
  2. 把旧空间中的元素一个个搬到新空间:
    • 如果 T 有移动构造 → 调用移动构造(我们会看到"移动构造 buffer")
    • 如果 T 只有拷贝构造 → 调用拷贝构造
  3. 析构旧空间中的对象
  4. 插入新的那个元素

因为我们给 buffer 提供了移动构造,大部分标准库实现都会优先用移动构造来搬元素,性能更好。

这就是右值引用 / 移动构造在容器内部的一个重要使用场景:
让容器扩容时不用对每个元素做深拷贝,而是快速"挪指针"。

3.5. 完美转发

3.5.1.写一个"转发函数"会丢掉左右值的信息

想象有一个需求:

写一个函数void F(...),然后写一个函数Relay(...)记录日志后再把参数原样转发给F`

cpp 复制代码
void F(int& x) { cout << "F(int& 左值)" << endl; }
void F(const int& x) { cout << "F(const int& 左值)" << endl; }
void F(int&& x) { cout << "F(int&& 右值)" << endl; }
void F(const int&& x) { cout << "F(const int&& 右值)" << endl; }

// 错误写法:
template<class T>
void Relay(T t) {
	cout << "Relay\n";
	F(t);	// 会发生什么?
}

int main() {
	int a = 10;
	Relay(a);		// 传左值
	Relay(10);		// 传右值

	return 0;
}

可能以为是:

  • Relay(a) -> F(int&)
  • Relay(10) -> F(int&&)

实际上:

  • 模板参数推导:

    1. Relay(a)时: T = int&, 参数类型按值传递,变成 T t -> int t

    2. Relay(10)时:T = int, T t -> int t

  • Relay里面,t是变量名,一定是一个左值

所以:

F(t)永远只能匹配到左值版本,右值属性丢失了!

3.5.2.解决方案:万能引用+完美转发

万能引用

写成这样:

cpp 复制代码
template <class T>
void Relay(T&& t){ F(t);}

T&&在模板参数推导的场景下是万能引用,既能接收左值,也能接收右值:

  • 传左值:T推导为int&,形参类型由int&&折叠为int&
  • 传右值:T推导为int,形参类型就是int&&

现在还有个问题:F(t)里面t还是左值

完美转发:根据T恢复原本的类型

std::forward做的事情是:根据模板参数T,判断传进来的是左值还是右值,然后把t转成对应的值类型

可以这样写:

cpp 复制代码
template <class T>
void Relay(T&& t){ 
    F(std::forward<T>(t));
}
完整测试
cpp 复制代码
// 正确写法:万能引用+完美转发

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

// 错误示范:丢失右值
template<class T>
void BadForward(T&& t) {
	cout << "BadForward(T&& t):" ;
	F(t);		// t永远是左值
}

// 完美转发
template<class T>
void PerfectForward(T&& t) {
	cout << "PerfectForward(T&& t):";
	F(forward<T>(t));
}

int main() {
	int a = 10;
	const int ca = 20;

	cout << "=== 传左值a ===\n";
	BadForward(a);
	PerfectForward(a);

	cout << "\n=== 传const左值ca ===\n";
	BadForward(ca);
	PerfectForward(ca);

	cout << "\n=== 传右值10 ===\n";
	BadForward(10);
	PerfectForward(10);

	cout << "\n=== 传const右值10 ===\n";
	BadForward(10);
	PerfectForward(10);

	return 0;
}

最后的输出结果:

css 复制代码
=== 传左值a ===
BadForward(T&& t):F(int& 左值)
PerfectForward(T&& t):F(int& 左值)

=== 传const左值ca ===
BadForward(T&& t):F(const int& 左值)
PerfectForward(T&& t):F(const int& 左值)

=== 传右值10 ===
BadForward(T&& t):F(int& 左值)
PerfectForward(T&& t):F(int&& 右值)

=== 传const右值10 ===
BadForward(T&& t):F(int& 左值)
PerfectForward(T&& t):F(int&& 右值)

4.完美转发的应用------容器的emplace系列

4.1. 核心意义:原地构造+完美转发

而对于push_back/insert:先把传进来的参数变成一个临时对象,再拷贝/移动进容器

例如:

cpp 复制代码
vector<string> v;
// push_back
v.push_back(string("111"));	// 构造临时string 再移动进vector

// emplace
v.emplace_back("111");		// 直接在vector的内存里构造string("111")

从语义上讲:

  • push_back:给我一个 已经构造好的 T 对象,我把它(拷贝/移动)到容器里
  • emplace_back:给我一堆 构造 T 所需的参数 ,我在容器那块内存直接 new 一个 T(args...)

这就是"少了一次临时对象"的差别。

所以:真正的使用场景如下:

  • 传的是构造参数如字面常量,多个内置类型,而不是已经构造好的对象
  • 容器元素需要深拷贝的对象,如string、自定义类、map的value

底层模板模式:

cpp 复制代码
template <class... Args>
void emplace_back(Args&&... args){
    ::new (end()) T(std::forward<Args>(args)...);
}
  • Args&&...:万能引用
  • std::forward<Args>(args)...:完美转发
  • ::new (end()):原地构造

4.2. 使用场景

4.2.1. 场景一:vector::emplace_backVSpush_back

cpp 复制代码
struct Widget {
	Widget(int x, int y) {
		cout << "构造Widget(" << x << "," << y << ")\n";
	}
	Widget(const Widget&) {
		cout << "拷贝构造Widget\n";
	}
	Widget(Widget&&) noexcept {
		cout << "移动构造Widget\n";
	}
};

int main() {
	vector<Widget> v;
	v.reserve(2);

	cout << "push_back\n";
	v.push_back(Widget(1, 2));

	cout << "emplace_back\n";
	v.emplace_back(1, 2);

	return 0;
}

逻辑执行:

  • push_back(Widget(1,2))
    1. 构造一个临时 Widget(1,2) → 打印"构造 Widget(1,2)"
    2. push_back 把这个临时对象移动构造进 vector → 打印"移动构造 Widget"
    3. 临时对象析构
  • emplace_back(1,2)
    • 在 vector 尾部的那块内存上直接调用 Widget(1,2) → 只打印"构造 Widget(1,2)"

差别:

  • push_back:构造 + 移动
  • emplace_back:只构造一次

大对象/复杂对象来说,少一次构造 + 移动,C++11 下是有价值的。

什么时候emplace_back没优势?

cpp 复制代码
Widget w(1, 2);
v.push_back(w);				// 左值:拷贝构造
v.emplace_back(w);			// 拷贝构造

v.push_back(move(w));		// 右值:移动构造
v.emplace_back(move(w));	// 移动构造

当已经存在了一个Widget对象,再使用emplace_back,本质上还是拷贝/移动构造,真正有意义的是:不先构造对象,直接给参数: v.emplace_back(1,2);

4.2.2. 场景二:map::emplaceVS insert

使用方式:

cpp 复制代码
map<int, string> m;

// 写法一: 传pair所需的参数
m.emplace(2, "hh");

// 写法二: make_pair
m.emplace(make_pair(2, "hh"));

m.insert(make_pair(2,"hh"));

对于m.emplace(2,"hh"),底层大概发生如下:

cpp 复制代码
node* newnode = allocate_node();
::new (&newnode->value) std::pair<const int, std::string>(2, "hh"); // 原地构造
link_to_tree(newnode);

对于m.insert(std::make_pair(2, "world")); 大概是:

  1. 先构造一个临时 std::pair<int, std::string>(2, "world")
  2. 再用它拷贝/移动构造节点内部的 std::pair<const int, std::string>

所以:

  • emplace(key, value_args...) → 少一次临时 pair
  • emplace(make_pair(...)) → 和 insert 差不多,优势变小

4.3. 底层机制

所有容器的 emplace(C++11)基本遵循同一个模式:

cpp 复制代码
template<class... Args>
iterator emplace(Args&&... args) {
    // 1. 申请 / 找到一块原始内存(vector 尾部、list 节点、map 节点等)
    void* p = allocate_raw_memory_for_one_T();

    // 2. 在这块内存上原地构造对象(placement new + 完美转发)
    ::new (p) T(std::forward<Args>(args)...);

    // 3. 把这个节点/元素挂到容器结构里(尾部、链表、红黑树、哈希表)
    link_to_container(p);

    return iterator_to(p);
}

要意识到:

  • Args&&...万能引用
  • std::forward<Args>(args)...完美转发,保证右值参数保持右值、左值保持左值
  • ::new (p) T(...)placement new,在已有内存上构造对象,不再 malloc

emplace 就是把这套模板模式应用到"插入元素"这个场景里。

那么在使用时就可以注意到:

  1. 有构造参数时 → 用 emplace

    cpp 复制代码
    vector<std::string> v;
    v.emplace_back("hello");        
    // 优于 push_back(std::string("hello"));
    
    map<int, std::string> m;
    m.emplace(1, "hello");          
    // 优于 insert(std::make_pair(1,"hello"));
  2. 已经有对象时 → push / emplace 都行

    cpp 复制代码
    std::string s = "hi";
    v.push_back(s);       // 拷贝
    v.emplace_back(s);    // 也是拷贝
  3. 对 node-based 容器(list/map/unordered_map)

    • emplace 优势更纯粹一些(节点本来就分配,新元素原地构造,旧元素不动)
    • 但仍然遵守上面两条规则

5. 可变参数模板

5.1. 是什么?

可变参数模板 = 模板参数可以是"0~N个"的一包东西,编译器会根据传入的实参个数和类型进行推导和展开

最基本的模板长这样:

c++ 复制代码
template <class... Args>
void Func(Args... args);
  • class... Args:类型包
  • Args... args:参数包(函数形参包)
  • 调用时,每个实参推导出一个类型,按照顺序装进Args...
c++ 复制代码
template <class... Args>
void Debug(Args... args) {
	// 这个写法很怪 sizeof...()	而其他是跟在class Args后面
	cout << "参数个数: " << sizeof...(Args) << endl;
}
int main() {
	Debug(1, 3.5, "hi", 'a');

	return 0;
}

对于Debug(1,3.5,"hi",'a');,编译器会做如下处理:

  1. 看到模板Debug(Args... args)和调用了4个实参->Args...里面就必须有四个类型
  2. 对每个实参做普通模板的推导:
    • 1->int
    • 3.5->double
    • "hi"->const char*
    • 'a'->char
  3. 得到:Args... = <int, double, const char*, char>
  4. 实例化出具体版本:
c++ 复制代码
void Debug<int, double, const char*,char>(int, double, const char*,char);

每个实参 → 一个类型 T_i → 按顺序排进 Args...,长度 = 实参数量。

5.2. 怎么用?

5.2.1. 参数包只能展开,不能索引

不能写args[0],只能用...拆包

一次性展开到一个函数里
c++ 复制代码
// 一次性展开到一个函数里
template <class... Args>
void call(void(*f)(Args...), Args... args) {
	f(args...);		// 展开成f(a,b,c...)
}
initial_list展开打印
c++ 复制代码
// 用initial_list展开打印
template<class... Args>
void Print(Args... args) {
	int arr[] = { (cout << args << " ",0)... };
	(void)arr;
    // 数组只是为了展开参数包,并不会使用,这里明确告诉编译器,arr是故意不用的,别报警告
	cout << endl;
}
int main() {
	Print(1, 3.5, "hh", 'a');

	return 0;
}

详细拆解一下这段代码:

这段代码的核心是用一个临时数组arr来展开参数包

  • 参数包Args... args是多个参数的集合,必须用...展开

  • 展开需要上下文,这里用到的是初始化列表{...}

  • { (cout << args << " ",0)... }会将每一个args展开为一个表达式:

    c++ 复制代码
    (cout<<参数1)<<" ",0),
    (cout<<参数2)<<" ",0),
    (cout<<参数3)<<" ",0),
    (cout<<参数4)<<" ",0),

    然后将这四个值放入数组arr

  • 为什么表达式要加", 0"?

​ 因为初始化列表的元素必须要是同一类型,这里用了逗号表达式,返回最右边值的类型,将每项强制转成``int`类型,并且保留打印的功能

  • 为什么用数组?

​ 因为数组初始化列表会按照顺序执行每个元素的初始化,利用"顺序执行"特性,让cout打印顺序正确

  • arr数组本身并不重要,他的唯一目的是:让每个(cout<<args<<...)被依次执行

5.2.2. 搭配右值引用

这是模板:

cpp 复制代码
// 搭配右值引用
template <class... Args>
void ForwardTo(FuncType f, Args&&... args) {
	// 这个...又tm写到外面去了
	f(forward<Args>(args)...);
}

Args&&...: 万能引用,既能接收左值也能接收右值

forward<Args>(args)...:对每个参数恢复它原来的左值/右值属性->完美转发

配合容器的emplace:

c++ 复制代码
#include <vector>
template<class... Args>
void vector<T>::emplace_back(Args&&... args) {
    ::new ((void*)_finish) T(std::forward<Args>(args)...);
    ++_finish;
}

可变参数模板 + Args&& + std::forward 就是 emplace 的逻辑:支持任意个构造参数,并保持每个参数原本的左值/右值属性,在容器内部原地构造对象

6. lambda表达式

6.1. 基本语法和用法

基本形式:

cpp 复制代码
[capture_list](parameter_list) mutable ->return_type{
    body;
};
  • [capture_list]:捕获列表,编译器根据[]来判断接下来的代码是否为lambda函数,捕获列表可以捕获上下文中的变量供lambda使用
  • (parameter_list):参数列表,和普通函数的参数列表一直,如果不传参,可以连同()一起省略
  • mutable:默认情况,lambda是一个const函数,mutable可以取消常性。使用这个修饰符的时候,参数列表不能省略
  • -> return_type:返回值类型
  • {body;}:函数体,可以使用参数列表和捕获到的变量

最常见的:

cpp 复制代码
auto f = [](int x) {return x * 2 };
int y = f(10);

// 算法里典型用法:
vector<int> v = {1,12,37,4,3};
sort(v.begin(),v.end(),[](int a, int b){ return a < b; });

这里的[](int a, int b){...0}就是一个比较函数对象

6.2. 底层原理

lambda本质是一个编译器帮忙生成的匿名函数对象类[]捕获的东西变成这个类的成员,函数体变成仿函数operator()

比如说:

cpp 复制代码
int factor = 10;
auto f = [factor](int x){return x * factor;};

编译器大致会生成一个类似这样的东西(伪代码):

cpp 复制代码
struct __Lambda_1 {
    int factor;                   // 捕获的变量变成成员

    __Lambda_1(int f) : factor(f) {}

    int operator()(int x) const { // 函数体变成 operator()
        return x * factor;
    }
};

int factor = 10;
__Lambda_1 f(factor);             // 捕获时拷贝 factor
int y = f(3);                     // 调用 operator()(3)

6.3. 捕获

6.3.1. 按值捕获[=]/[x]

cpp 复制代码
void captureValue() {
	int a = 10;
	// a 是复制进来的,lambda 里面访问的是那份拷贝
	auto f = [a]() { cout << a << endl; };

	a = 20;
	f();

	int x = 1, y = 2, z = 3;
	auto g = [=] {cout << x << ":" << y << ":" << z << endl; };

	g();
}

按值捕获会 拷贝一份当前的值 到 lambda 对象里,后面原变量变了,不影响 lambda已经拷贝的那份

6.3.2. 按引用捕获[&]/[&X]

cpp 复制代码
// 按引用捕获
void captureRef() {
	int a = 10;
	// lambda 里面访问的是a的别名
	auto f = [&a] {cout << a << endl; };

	a = 20;
	f();

	int x = 1, y = 2, z = 3;
	auto g = [&] {cout << x << ":" << y << ":" << z << endl; };

	g();
}

按引用捕获的是别名,对外部变量的修改、外部对变量的修改,都互相可见

危险点:如果外部变量已经被销毁了引用就悬空了

6.3.3. 混合捕获

cpp 复制代码
// 混合捕获
void captureMixing() {
	int a = 10, b = 20;
	double c = 0.6;
	// 错误写法 C++要求 默认方式捕获必须放到最前面,之后的特殊捕获随意
	//auto f = [&b,=] {cout << a << ":" << b << ":" << c << endl; };
	auto f = [=,&b] {cout << a << ":" << b << ":" << c << endl; };

	f();
}

这里需要注意:默认捕获 要放到[]的最前面,之后个性化的捕获放后面

6.4. mutableconst

默认情况下,按值捕获的lambdaoperator()const的:

cpp 复制代码
void test() {
	// 按值捕获的lambda是const的
	int a = 10;
	// error C3491: "a": 无法在非可变 lambda 中修改通过复制捕获
	auto f = [a]() {a++; };
}

如果想在lambda内部修改按值捕获的拷贝,要加mutable

cpp 复制代码
int a = 10;
auto f = [a]()mutable {
	// 改的是拷贝 不是外面的a
	a++; cout << a << endl;
	};
f();	// 11
f();	// 12
cout << a << endl;	// 10

重点:

  • mutable 允许在 lambda 里修改"捕获的副本",而不是原变量
  • 不加 mutablelambdaoperator()const 成员函数,不能改成员(也就不能改按值捕获的变量)

6.5. lambda和函数对象

lambda = 仿函数 + 捕获能力 + 更好写法。

对于仿函数:

cpp 复制代码
struct Rate{
    double operator()(double money,int year) const{
        return money * pow(1.05,year);
    }
}
Rate r;
r(100,3);

对于lambda

cpp 复制代码
auto r2 = [](double money, int year){
    return money * pow(1.05,year);
}
r2(100,3);

底层都是一个对象调用了operator[]

总结:lambda 和仿函数有什么区别?

lambda 本质上是编译器自动为生成的"匿名仿函数对象",内部是一个结构体 + operator()

和手写仿函数相比,lambda` 更简洁、支持捕获上下文变量、默认 `operator()const、更容易内联优化。

两者本质相同,都是"可调用对象",但 lambda 更适用于一次性的闭包场景,而仿函数更适用于需要反复复用的策略对象。

7. 包装器

7.0. 什么是可调用对象?

对于这段代码:

cpp 复制代码
ret = func(x);

func可能是什么?在C++中,能写成func(...)的,都可以叫可调用对象,包括了:

  • 普通函数
  • 函数指针
  • 仿函数对象
  • lambda表达式对象

而包装器,就是用一个统一的类型装下这些可调用对象

7.1. 模板函数的问题

先看一段代码:

cpp 复制代码
template<class F, class T>
T useF(F f, T x)
{
    static int count = 0;
    std::cout << "count:" << ++count << std::endl;
    std::cout << "count:" << &count << std::endl;
    return f(x);
}
double f(double i) { return i / 2; }

struct Functor {
    double operator()(double d) { return d / 3; }
};

// 用三种方式调用
int main() {
    // 函数名
    std::cout << useF(f, 11.11) << std::endl;

    // 函数对象(仿函数)
    std::cout << useF(Functor(), 11.11) << std::endl;

    // lambda 表达式
    std::cout << useF([](double d)->double { return d / 4; }, 11.11) << std::endl;
}

虽然useF看起来只有一个模板

但是:

  • 第一次调用:F = double(*)(double)

  • 第二次调用:F = Functor

  • 第三次调用:F =(某个 lambda 的匿名类型)

编译器实例化出了3个不同版本的useF

全局变量count有三个不同的地址,这种模式到处都是,在一个项目里,模板实例化会将项目变得很大,能不能有一种方式,把这些调用统一成一种类型?

7.2. 包装器

包装器的原型是:

cpp 复制代码
template <class Ret, class... Args>
class function<Ret(Args...)>;

std::function<Ret(Args...)> 就是一个"函数包装器类",能包装任何可以被当成 Ret(Args...) 来调用的东西。

cpp 复制代码
#include <functional>

int f(int a, int b) { return a + b; }

struct Func {
	int operator()(int a, int b) { return a + b; }
};

class Plus {
public:
	static int plusi(int a, int b) { return a + b; }
	double plusd(double a, double b) { return a + b; }
};

int main() {
	// 1. 包普通函数/函数指针
	function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;

	// 2. 包仿函数
	function<int(int, int)> func2 = Func();
	cout << func2(1, 2) << endl;

	// 3.包lambda
	function<int(int, int)> func3 =
		[](int a, int b) {return a + b; };
	cout << func3(1, 2) << endl;

	// 4. 包静态成员函数
	function<int(int, int)> func4 = &Plus::plusi;
	cout << func4(1, 2) << endl;

	// 5. 包普通成员函数 需要把类也当参数传进去
	function<double(Plus,double, double)> func5 = &Plus::plusd;
	cout << func5(Plus(),1.0, 2.0) << endl;

	return 0;
}

同一个 std::function<int(int,int)>
可以装 函数 / 函数指针 / 仿函数 / lambda / 成员函数指针。

这就是它叫"包装器(适配器)"的原因:它帮你把不同种类的可调用对象适配成统一的类型

7.3. 用包装器改造useF

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

// --------------------------------------------------------------------------
// 包装器版 useF:统一要求 "f 是一个能 T(T) 调的可调用对象"
// --------------------------------------------------------------------------
template <class T>
T useF(function<T(T)> f, T x) {
    static int count = 0;         // 静态变量:整个模板只实例化一次,只存在一份
    cout << "count:" << ++count << endl;
    cout << "count:" << &count << endl;

    // 调用包装器 f,它里面可能存着函数、lambda、仿函数
    return f(x);
}

// --------------------------------------------------------------------------
// 一个普通函数 ------ 能被当成 double(double) 调用
// --------------------------------------------------------------------------
double f(double i) {
    return i / 2;
}

// --------------------------------------------------------------------------
// 一个仿函数(函数对象) ------ 重载 operator(),可以 f(d) 调用
// --------------------------------------------------------------------------
struct Functor {
    double operator()(double d) {
        return d / 3;
    }
};

int main()
{
    // ----------------------------------------------------------------------
    // 把不同类型的可调用对象"包装"成同一种类型
    // function<double(double)> 就是一个固定类型的"可调用对象包装器"
    // ----------------------------------------------------------------------

    function<double(double)> func1 = f;                // 包普通函数:f(double)
    function<double(double)> func2 = Functor();        // 包仿函数对象 Functor()
    function<double(double)> func3 = [](double d) {
        return d / 4;                                  // 包 lambda(匿名函数)
    };

    // ----------------------------------------------------------------------
    // 三次 useF 调用,参数类型完全一样:
    //     useF<double>(function<double(double)>, double)
    //
    // 所以模板 useF 只实例化一份,静态变量 count 只有一个。
    // ----------------------------------------------------------------------

    cout << useF(func1, 11.11) << endl;   // f(11.11) = 5.555
    cout << useF(func2, 11.11) << endl;   // Functor::operator()(11.11) = 3.7033
    cout << useF(func3, 11.11) << endl;   // lambda 的结果

    return 0;
}

useF 的参数类型固定为 std::function<double(double)>

  • 不管传的是函数 / 仿函数 / lambda
  • 对编译器来说,模板只实例化一次(T = double)

模板版本:编译期多态 ,性能最好,但容易导致模板膨胀
std::function 版本:统一成一个类型,只实例化一次

7.4. 迷你包装器实现

cpp 复制代码
template<class R, class... Args>
class MiniFunction {
    void* obj;                                           // 存真实对象
    R (*caller)(void*, Args&&...);                       // 怎么调用

public:
    template<class F> MiniFunction(F&& f)                // 构造:存对象 + 存调用器
        : obj(new F(std::forward<F>(f)))
        , caller([](void* p, Args&&... as) -> R {        // lambda 作为"调用器"
            return (*static_cast<F*>(p))(std::forward<Args>(as)...);
        }) {}

    R operator()(Args... as) {                           // 对外调用统一接口
        return caller(obj, std::forward<Args>(as)...);
    }
};
相关推荐
想唱rap1 小时前
C++ map和set
linux·运维·服务器·开发语言·c++·算法
小欣加油2 小时前
leetcode 1018 可被5整除的二进制前缀
数据结构·c++·算法·leetcode·职场和发展
玖剹3 小时前
递归练习题(四)
c语言·数据结构·c++·算法·leetcode·深度优先·深度优先遍历
西部秋虫4 小时前
YOLO 训练车牌定位模型 + OpenCV C++ 部署完整步骤
c++·python·yolo·车牌识别
雾岛听蓝6 小时前
C++ 类和对象(一):从概念到实践,吃透类的核心基础
开发语言·c++·经验分享·笔记
Dream it possible!6 小时前
LeetCode 面试经典 150_图_克隆图(90_133_C++_中等)(深度优先:DFS)
c++·leetcode·面试·
鸭子程序员7 小时前
c++ 算法
开发语言·c++·算法
不会c嘎嘎7 小时前
算法百练,直击OFFER -- day5
c++·算法
序属秋秋秋7 小时前
《Linux系统编程之进程环境》【环境变量】
linux·运维·服务器·c语言·c++·操作系统·系统编程