C++11:可变参数模板,lambda,function包装器
文章目录
- 可变参数模板
- 参数包
- 包的展开方式(递归展开和包扩展)
- 递归展开
- 包扩展
- empalce系列接口
- Lambda
- Lambda表达式的基本语法
- 捕获列表
- function包装器
- bind
可变参数模板
C++ 的可变参数模板(Variadic Templates)是 C++11 引入的重要特性,允许模板参数接受任意数量、任意类型的参数,极大增强了模板的灵活性。它广泛用于泛型编程中需要处理不确定数量参数的场景(如容器初始化、函数参数转发等)
参数包
可变数目的参数被称为参数包,存在两种参数包:
-
模板参数包,表示零或多个模板参数。(如
class ...Args
),在模板参数列表中,class...
或typename...
指出接下来的参数表示零或多个类型列表 -
函数参数包:表示零或多个函数参数。(如
Args... args
),在函数参数列表中,类型名后面跟...
指出接下来表示零或多个形参对象列表
template<class ...Args>
void func(Args... args){ }template<class ...Args>
void func(Args&... args) {}template<class ...Args>
void func(Args&&... args) {}
函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
下面用 sizeof...
运算符配合可变参数模板来计算参数的个数:
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("xxxxx"), x); // 包里有3个参数return 0;
}
程序运行如下:
0
1
2
3
编译器编译时会看有没有可变参数模板,通过实现多个函数模板来实现上述功能,最终实例化成下述四个函数模板.
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);
包的展开方式(递归展开和包扩展)
参数包不能直接使用,必须通过展开操作(...
),将其分为独立的参数.
递归展开
最常见的用法是通过递归函数模板展开参数包:
- 定义一个递归终止函数(处理参数包为空的情况).
- 定义一个可变参数模板,每次处理参数包的第一个元素,剩余元素继续递归.
下面是一个打印任意数量、任意类型参数的例子:
// 1. 递归终止函数(参数包为空时调用)
void Print()
{cout << "参数打印完毕!" << endl;
}// 2. 可变参数函数模板(递归展开)
// 每次处理第一个参数t,剩余参数包Args... args继续递归
template<class T, class ...Args >
void Print(T t, Args... args)
{cout << "参数: " << t << "(剩余" << sizeof...(args) << "个参数)" << endl;Print(args...); // 展开剩余参数包,继续递归
}int main() {Print(10, 3.14, string("hello"), 'A'); // 传入4个不同类型的参数return 0;
}
main
函数中调用 Print
函数,调用参数包,参数包第一个参数会传递给 t
,剩余的参数传递给 args
.
之后继续递归,直至无参数传递给 Print
,此时调用递归终止函数,函数调用完毕,参数包展开完毕.
程序运行结果如下:
参数: 10(剩余3个参数)
参数: 3.14(剩余2个参数)
参数: hello(剩余1个参数)
参数: A(剩余0个参数)
参数打印完毕!
具体函数模板实例化后调用过程如下图:
包扩展
除了递归,C++11 后还可以通过包扩展直接展开参数包(无需递归),例如初始化容器:
// 可变参数模板函数:用参数包初始化vector
template<class ...Args>
vector<int> make_vector(Args... args)
{// 将参数包args...展开为独立参数,初始化vectorreturn { args... };
}int main()
{auto vec = make_vector(1, 2, 3, 4, 5);for (int num : vec){cout << num << " ";}return 0;
}
当调用 make_vector(1,2,3,4,5)
时,编译器会做一下几件事:
-
推导模板参数包
函数调用的实参是
1,2,3,4,5
,都是int
类型.因此编译器会推导模板参数包Args...
为int,int,int,int,int
-
实例化具体函数
编译器会根据推导结果,生成一个针对
Args={int, int, int, int, int}
的具体函数,相当于:std::vector<int> make_vector(int arg1, int arg2, int arg3, int arg4, int arg5) {return {arg1, arg2, arg3, arg4, arg5}; // 包扩展后的值 }
-
调用实例化的函数
main
函数中的调用会直接匹配这个实例化的函数,传入1,2,3,4,5
作为实参,最终用这 5 个值初始化std::vector<int>
.
empalce系列接口
在 C++ 标准库中,
emplace
系列接口(如emplace_back
、emplace
、emplace_front
等)是 C++11 引入的高效插入元素的方法,主要用于容器(如std::vector
、std::list
、std::map
等)。它们的核心优势是直接在容器内部构造元素,避免了不必要的临时对象复制或移动,从而提升性能。
emplace
系统接口依赖可变参数模板和完美转发实现
- 通过可变参数模板接收任意数量,任意类型的参数
- 通过
std::forward
将参数完美转发给元素的构造函数,确保参数的左值/右值属性被正确传递
移动语义的开销本身就不大,故 emplace_back
与 push_back
在传右值时开销差不多,但是在传递左值时,push_back
无法使用移动语义,此时必须调用对应的拷贝构造函数进行深拷贝.
而 emplace_back
可以直接传递其可变参数包,使得传递的参数直接在已经分配好的内存空间上直接构造.
struct MyString {char* _data;size_t _size;// 构造函数(分配内存)MyString(const char* str = ""):_size(strlen(str)) {_data = new char[_size + 1];memcpy(_data, str, _size+1);cout << "构造函数:分配" << _size + 1 << "字节\n";}// 移动构造函数(转移资源,不分配新内存)MyString(MyString&& other) noexcept:_data(other._data), _size(other._size) {other._data = nullptr;other._size = 0;cout << "移动构造:资源转移\n";}// 禁止复制(突出移动的作用)MyString(const MyString&) = delete;MyString& operator=(const MyString&) = delete;~MyString() {if (_data) {cout << "析构函数:释放" << _size + 1 << "字节\n";delete[] _data;}}
};int main()
{vector<MyString> vec;vec.reserve(2);cout << "emplace_back" << endl;vec.emplace_back("hello");cout << "emplace_back" << endl;cout << "push_back" << endl;vec.push_back("world");cout << "push_back" << endl;return 0;
}
reverse(2)
确保 vector
有足够空间,.
emplace_back
直接在容器的内存中调用 MyString
的构造函数,没有临时对象,不会触发移动构造.
push_back
即使在容器内存足够的情况下,还是先调用 MyString
的构造函数,创建临时对象,之后触发移动构造,将已有元素移动到容器内存中.
程序运行结果如下:
emplace_back
构造函数:分配6字节
emplace_back
push_back
构造函数:分配6字节
移动构造:资源转移
push_back
析构函数:释放6字节
析构函数:释放6字节
由此可以看到,在有移动构造的情况下,参数为左值时,emplace_back
相比 push_back
的性能提升在构造需要深拷贝的对象.如果是需要构造仅需浅拷贝的对象,此时性能提升就明显了,没有了移动构造,push_back
需要构造和拷贝构造两次,比 emplace_back
要多一次.
Lambda
C++11 引入的 Lambda 表达式(匿名函数)是一种便捷的函数定义方式,可在需要函数对象的地方直接编写代码,无需单独定义函数或函数对象类。它广泛用于算法(如
std::for_each
)、回调函数、多线程等场景,极大简化了代码。
Lambda表达式的基本语法
Lambda 的完整语法结构如下:
[capture-list] (parameter-list) mutable noexcept -> return-type {// 函数体
}
各部分含义:
- 捕获列表(capture-list) :用于捕获 Lambda 外部的变量(在函数体中使用),是 Lambda 与外部作用域交互的关键。常见形式:
[],[=],[&],[x,&y],[this]
- 参数列表(parameter-list) :与普通函数的参数列表一致,可省略(无参数时)。
- mutable(可选) :允许修改按值捕获的变量(默认按值捕获的变量是
const
的)。 - noexcept(可选) :声明 Lambda 不会抛出异常。
- 返回类型(return-type) :通常可省略,由编译器自动推导(若函数体只有
return
语句)。 - 函数体 :Lambda 的执行逻辑。
下面是简单的Lambda示例:
int main()
{// 直接定义并且调用[]{cout << "简单Lambda" << endl; }();// 带参数和返回值auto add = [](int a, int b)-> int {return a + b;};cout << add(1, 2) << endl;// 按值捕获外部变量int x = 10;auto printX = [x] { cout << x << endl; };printX();return 0;
}
程序运行结果如下:
简单Lambda
3
10
直接调用时, 此时 []{cout << "简单Lambda" << endl; }
为一个对象,由于该Lambda表达式没有参数,所以调用时最后加 ()
即可.
auto
用于接收Lambda表达式(其类型是编译器生成的匿名类型,无法显式写出).
// 在Linux下做测试
// 打印上述printX的名称
cout << typeid(printX).name() << endl;
root:~/learning# ./test
Z4mainEUlvE_
捕获列表
Lambda 的捕获列表(capture list) 是 Lambda 表达式与外部作用域变量交互的核心机制,用于指定如何捕获外部变量供 Lambda 内部使用。捕获列表位于 Lambda 表达式的最开头(
[]
内),决定了外部变量的访问方式(按值 / 按引用)和范围。
捕获列表的基本规则:
- 捕获列表仅能捕获 Lambda定义时所在作用域的局部变量(全局变量无需捕获即可直接使用)。
- 捕获方式分为按值捕获和 按引用捕获 ,行为差异显著。
- 可显式指定捕获特定变量,或使用默认捕获模式。
常见形式:
[]
:不捕获任何变量。[=]
:按值捕获所有外部变量(只读)。[&]
:按引用捕获所有外部变量(可修改)。[x, &y]
:按值捕获x
,按引用捕获y
。[this]
:捕获当前类的this
指针(用于类成员函数中访问成员变量)。
下面是常用捕获方式及示例:
#include <iostream>using namespace std;int main()
{// f1:空捕获:无法访问xint x = 10;auto f1 = [] {// cout << x; // 错误:未捕获xcout << "无外部变量\n";};f1();// f2,f3:按值捕获或默认按值[=]int a = 10, b = 20;// 捕获a和b的副本(显式指定)auto f2 = [a, b]() {cout << a + b << endl;};// 默认按值捕获所有外部变量(等价于捕获a和b)auto f3 = [=]() {cout << a * b << endl;};f2();f3();// f4:配合mutable修改副本auto f4 = [x]() mutable {x = 5; // 允许修改副本(仅内部有效)std::cout << "内部x=" << x; // 输出5};f4();std::cout << "外部x=" << x << endl; // 输出10(原变量不变)// f5,f6:按引用捕获 [&变量名] 或默认按引用 [&]int c = 30, d = 40;// 显式按引用捕获c和dauto f5 = [&c, &d]() {c = 35;d = 50; // 直接修改};// 默认按引用捕获所有外部变量auto f6 = [&] {cout << c + d << endl;};f5();f6();// f7,f8:混合捕获x = 1;int y = 2, z = 3;// 默认按值捕获所有,仅y按引用捕获auto f7 = [=, &y] {// x = 10; // 错误:x按值捕获,不可修改y = 20; // 正确:y按引用捕获,可修改cout << x + y + z << endl; // 1+20+3=24};// 默认按引用捕获所有,仅z按值捕获auto f8 = [&, z] {x = 10; // 正确:x按引用捕获// z = 30; // 错误:z按值捕获,不可修改};f7();f8();// f9:捕获this指针 [this](类内使用)class MyClass {private:int num = 100;public:void test() {// 显式捕获thisauto f8 = [this] {num = 200; // 访问并修改成员变量cout << num << endl;};f8(); // 输出200}};MyClass obj;obj.test();return 0;
}
程序运行如下:
无外部变量
30
200
内部x=5外部x=10
85
24
200
需要注意的是:
- 当使用混合捕捉时,第一个元素必须是
&
或=
,并且&
混合捕捉时,后面的捕捉变量必须是值捕捉,同理=
混合捕捉时,后面的捕捉变量必须是引用捕捉。 - lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
function包装器
std::function
是 C++11 引入的函数包装器(位于<functional>
头文件中),用于 包装各种可调用对象 (函数、Lambda 表达式、函数指针、函数对象、成员函数指针等),将它们统一为一种类型,方便存储、传递和使用。
核心作用
C++ 中 “可调用对象” 的类型千差万别(如普通函数的类型、Lambda 的匿名类型、函数对象的自定义类型等),std::function
可以将这些不同类型的可调用对象 包装成同一种类型 ,解决了 “类型不统一” 的问题。
基本语法
std::function<返回值类型(参数类型列表)> 变量名;
- 包装普通函数
// 普通函数
int add(int a, int b) {return a + b;
}int main() {// 包装普通函数function<int(int, int)> func = add;cout << func(3, 5) << endl; // 输出return 0;
}
-
包装lambda表达式
int main(){auto mul = [](int a, int b) { return a * b; };// 包装lambda表达式function<int(int, int)> func = mul;cout << func(3, 5) << endl; // 输出15return 0; }
-
包装函数对象(仿函数)
// 函数对象,重载operator struct Divide {int operator()(int a, int b) const {return a / b;} };int main() {// 包装函数对象function<int(int, int)> func = Divide();cout << func(10, 2) << endl; // 输出5return 0; }
-
包装类的成员函数
class Calculator { public:int sub(int a, int b) {return a - b;} };int main() {Calculator calc;// 包装成员函数,成员函数第一个参数为this指针function<int(Calculator*, int, int)> func1 = &Calculator::sub;function<int(Calculator, int, int)> func2 = &Calculator::sub;function<int(Calculator&&, int, int)> func3 = &Calculator::sub;// 调用,输出为8cout << func1(&calc, 10, 2) << endl;cout << func2(calc, 10, 2) << endl;cout << func3(move(calc), 10, 2) << endl;cout << func3(Calculator(), 10, 2) << endl;return 0; }
由于成员函数第一个参数隐含this指针,使用
function
进行包装时,要注意第一个参数不要落下,以上三种方式都可以作为参数.
bind
std::bind
是 C++11 引入的函数绑定工具(位于<functional>
头文件),用于 将可调用对象与部分参数预先绑定 ,生成一个新的可调用对象(“绑定器”)。它的核心作用是灵活调整函数的参数列表(如固定部分参数、调整参数顺序),适配不同场景的调用需求。
基本语法
std::bind(可调用对象, 参数列表);
- 可调用对象 :可以是普通函数、Lambda、函数对象、成员函数指针等。
- 参数列表 :包含具体值或占位符(
std::placeholders::_1, _2, ...
),用于指定新函数的参数如何传递给原对象。
std::bind
的关键是通过通过占位符控制参数传递, _1
表示第一个未固定参数,_2
_3
等同理
通过下列形式引入占位符:
using namespace std::placeholders; // 引入占位符 _1, _2...
using std::placeholders::_1;
using std::placeholders::_2;
-
固定参数
将函数的部分参数预先固定,新生成的函数只需传入剩余参数。
// 原函数:计算a-b
int sub(int a, int b) {return a - b;
}int main() {// 绑定第二个参数为 5,新函数签名变为 int(int)(仅需传入 a)auto minus5 = bind(sub, _1, 5);cout << minus5(10) << endl; // 输出10// 绑定第一个参数为 20,新函数签名变为 int(int)(仅需传入 b)auto minusFrom20 = bind(sub, 20, _1);cout << minusFrom20(7) << endl; // 输出13return 0;
}
-
调整参数顺序
通过占位符重新排列参数传递的顺序。
// 原函数:a/b
double divide(double a, double b) {return a / b;
}int main() {// 交换参数顺序:新函数的 _1 传给原函数的 b,_2 传给原函数的 aauto invert_divide = bind(divide, _2, _1);cout << invert_divide(2, 10) << endl; // 相当于mul(10, 2)return 0;
}
-
绑定成员函数(需绑定
this
指针)非静态成员函数隐含
this
指针作为第一个参数,std::bind
需显式绑定对象(或对象指针 / 引用)。
class Math {
public:// 成员函数:计算 a * bint multiply(int a, int b) {return a * b;}
};int main() {Math math;// 绑定对象指针 &math 和成员函数,新函数签名为 int(int, int)auto bound_multiply = bind(&Math::multiply, &math, _1, _2);cout << bound_multiply(3, 4) << endl; // 等价于 math.multiply(3,4)
}