文章目录
- 模板可变参数模板
- 可变参数包的展开
- 可变参数包与STL容器中的emplace函数关系
- Lambda 表达式
- function 包装器
- function 包装器对成员函数的包装
- bind 绑定
模板可变参数模板
可变参数模板是C++11引入的一个特性,允许模板接收任意数量的参数;
该特性增加了C++的泛型编程能力;
可变参数模板引入可使用...
来表示模板参数包;
可在模板参数和函数参数中使用;
template<class ...Args>
void func(Args ...args)
其中class ...Args
表示其可变参数包的类型名为Args
;
func()
函数中的Args ...args
表示传入一个该可变参数包;
template <class ...Args>
void func1(Args ...args){// 代码
}int main() {func1(1, "666", 'a', 2.2);return 0;
}
-
可变参数包的参数类型个数
可通过
sizeof()
来查看当前传入的可变参数包的参数类型个数;template <class... Args> void func1(Args... args) {cout << sizeof...(args) << endl; }int main() {func1(1, "666", 'a', 2.2);return 0; }
通常在使用
sizeof()
对可变参数包的参数个数进行查看时语法规定必须使用sizeof...(可变参数包)
进行查看;运行结果为:
$ ./test 4
可变参数包的展开
可变参数包通常使用...
展开参数包,如:
func(args...);
当编译器遇到args...
时会将参数包中的每个参数都展开为独立的参数;
假设传入func()
函数的参数为'c',10,2.2
时,在函数func
中将优先获取第一个参数c
;
其参数展开的顺序按照它们在参数包中的顺序;
通常展开参数包的方法以递归,或是利用初始化数组使其进行展开;
-
递归方式展开
void _ShowList() { cout << endl; }template <class T, class... Args> void _ShowList(const T& t1, Args... args) {cout << typeid(t1).name() << " : " << t1 << endl; // 打印参数类型与参数值_ShowList(args...); }template <class... Args> void ShowList(Args... args) {_ShowList(args...); }int main() {ShowList(1, "666", 'a', 2.2);return 0; }
在这个例子中
ShowList()
函数是一个使用模板可变参数包的函数模板;_ShowList()
函数则是该函数的子函数,该函数将一个可变参数包衍生为一个T
类型的模板参数与一个可变参数包class ...Args
,并且该函数存在一个返回值与参数都为空的函数;调用
ShowList()
函数并传入1, "666", 'a', 2.2
为参数;根据可变参数包的展开原理,
_ShowList()
中的const T&
类型将优先获取可变参数包中的第一个参数,并将剩余的可变参数包以递归的形式调用逐个进行展开,当可变参数包中所有参数被展开完毕后将对应的调用无参的_ShowList()
函数重载;运行结果为:
$ ./test i : 1 PKc : 666 c : a d : 2.2
-
初始化数组形式展开
template <class T> int _Analysis(T t) {cout << t << endl;return 0; }template <class... Args> void Analysis(Args... args) {int arr[] = {_Analysis(args)...}; }int main() {Analysis(1, "666", 'a', 2.2);return 0; }
在这个例子中定义了一个可变模板参数包函数模板
Analysis()
,并定义了一个模板参数类型为T
的子函数模板_Analysis()
;传入参数并调用
Analysis()
函数,其将接收到这个可变参数包,并将其对数组进行初始化;此处的初始化采用列表初始化对数组进行初始化,在列表初始化中将根据可变参数包的数据个数依次调用
_Analysis()
函数并传入对应的可变参数包,最终实现可变参数包的展开;对应其展开时将会把
int arr[] = {_Analysis(args)...}
展开为:int arr[] = {_Analysis(1), _Analysis("666"), _Analysis('a'), _Analysis(2.2)};
运行结果为:
$ ./test 1 666 a 2.2
可变参数包与STL容器中的emplace函数关系
在 C++11 中对容器更新了对应的emplace
版本的函数;
如std::vector<>
与std::list<>
中的emplace_back
:
// vector
template <class... Args> void emplace_back (Args&&... args);// list
template <class... Args> void emplace_back (Args&&... args);
其中可变参数包允许函数接收任意数量和类型的参数,使得emplace_back
可以直接接收构造新元素所需的所有参数;
-
万能引用
其中函数调用中使用了万能引用
Args&&... args
使得函数既可以接收左值夜可以接收右值;
其中该函数的实现依靠了完美转发,使得其可以通过std::forward<Args>(args)...
保持可变参数包原有的属性将其转发至真正构造的部分,即直接在分配的内存上构造函数从而不需要再使用移动或拷贝构造,而是直接构造;
该函数的实现对于传递右值时与普通插入函数没有太大的差异,对于右值而言普通插入函数可通过移动构造的方式,即 构造+移动 ;
移动语义的开销本身就不是特别大,故emplace
版本插入函数与传右值时的普通插入函数效率大差不差,但在使用普通插入函数传递左值进行构造时无法进行移动语义,如移动构造或是移动赋值,此时必将调用对应的拷贝构造函数进行深拷贝;
而emplace
版本插入函数可通过直接传递其可变参数包使得直接将参数在分配的内存空间上直接构造;
从而避免了拷贝构造的开销;
Lambda 表达式
lambda
表达式是C++ 11 引入的一个新的特性,允许创建匿名函数对象;
Lambda
表达式提供一种较为简洁的方式定义行内函数,通常适用于需要短小函数的场景,如算法库中的回调函数;
[capture](parameters) mutable -> return_type { body }
-
[capture]
捕获列表捕获列表定义了
Lambda
表达式可以访问的外部作用域中的变量,其中该外部作用域指的是Lambda
表达式外部的作用域;其语法为:
-
[]
表示不捕获任何外部变量;
-
[=]
表示以传值的方式捕获所有外部变量;
通常以传值的方式捕获的变量在
Lambda
表达式中不允许被修改(与mutable
有关,mutable
为可选项,不使用mutable
时Lambda
中的参数带const
属性);当该
Lambda
为类内成员函数中的行内函数时,该方式可以传值的方式获取该类的this
指针(表示可直接以传值的方式访问该类内成员); -
[&]
表示以引用方式捕获所有外部变量;
通常以传引用的方式捕获的变量可在
Lambda
表达式中被修改,且与引用相同,同时在使用[&]
时可不需要声明mutable
;当该
Lambda
为类内成员函数中的行内函数时,使用[&]
为默认捕获时对this
指针依旧是传值捕获,这是一个特殊的捕获处理; -
[x,&y]
捕获列表可同时以不同的捕获方式或相同的捕获方式捕获多个特定的外部变量,但在捕获时不可重复以同一种方式捕获,如
[x,&x]
,[=,x]
或[=,&]
等; -
[this]
表示以传值捕获的方式捕获当前类的
this
指针; -
[=,&x]
表示以传值方式捕获所有外部变量,但对
x
以传引用捕获方式捕获;
-
-
(parameters)
参数列表参数列表与普通函数类似,可指定参数的类型与名称;
参数列表定义了
Lambda
表达式接收的输入参数;对应的如果
Lambda
没有参数可以省略()
或使用()
或在参数列表中传入void
; -
mutable
关键字该关键字允许在
Lambda
表达式内部修改通过传值捕获的变量;通常传值捕获的变量具有
const
属性,具有const
属性的变量无法被修改,而使用mutable
关键字进行修饰后则可以修改通过传值捕获的参数; -
-> return_type
Lambda
表达式可以显示指定该表达式返回的类型;通常该类型可直接省略,让编译器自动推导;
-
{ body }
Lambda
表达式的函数体是包含在花括号{}
内的代码块,用来定义Lambda
表达式的行为;该函数体与普通函数的函数体非常相似;
在函数体内可以使用捕获列表捕获的变量,当该
Lambda
表达式不存在返回值时可不使用return
对Lamdba
表达式进行返回;
Lambda
表达式本质上是一个仿函数,其底层的实现就是依靠仿函数,将先为该函数实例化出一个对象,再调用其对应的operator()()
运算符重载;
通常使用auto
接收Lambda
表达式所实例化的匿名函数对象,在Windows
中Lambda
表达式其函数名是一个以lambda_uuid
进行命名的一个仿函数(不同的系统对应命名的方式也不同);
int main() {auto f1 = []() { cout << "hello world" << endl; };f1();cout << typeid(f1).name() << endl;return 0;
}
在这个例子中使用Lambda
表达式创建了一个匿名函数对象,并用f1
,进行接收,同时打印出对应Lambda
表达式的类型名,省略了返回值类型与mutable
关键字,运行结果为;
# 此处以 Linux 做测试$ ./test
hello world
Z4mainEUlvE_
-
Lambda
表达式的使用int main() {int a = 10;int b = 20;// f1: 值捕获 a 和 b,只读访问auto f1 = [=]() { cout << a << " : " << b << endl; };f1(); // 输出: 10 : 20// f2: 引用捕获所有变量,可以修改 a 和 bauto f2 = [&]() {int tmp = a;a = b;b = tmp;};f2();cout << a << " : " << b << endl; // 输出: 20 : 10 (a 和 b 的值被交换)// f3: 值捕获 a 和 b,使用 mutable 允许修改捕获的副本,但不影响原始变量auto f3 = [a, b]() mutable {int tmp = a;a = b;b = tmp;};f3();cout << a << " : " << b << endl; // 输出: 20 : 10 (原始 a 和 b 不变)// f4: 值捕获 a,引用捕获 b,mutable 允许修改 a 的副本和 b 的原始值auto f4 = [a, &b]() mutable {int tmp = a;a = b;b = tmp;};f4();cout << a << " : " << b << endl; // 输出: 20 : 20 (只有 b 被修改为 a 的初始值)b = 5; // 修改 b 的值// f5: 引用捕获 a 和 b,可以直接修改原始变量auto f5 = [&a, &b]() {int tmp = a;a = b;b = tmp;};f5();cout << a << " : " << b << endl; // 输出: 5 : 20 (a 和 b 的值再次交换)// f6: 通过参数引用直接操作传入的变量auto f6 = [](int& x, int& y) {int tmp = x;x = y;y = tmp;};f6(a, b);cout << a << " : " << b << endl; // 输出: 20 : 5 (a 和 b 的值再次交换)return 0; }
这个例子展示了
Lambda
表达式的不同捕获方式和他们对变量的影响;其运行结果为(参考代码与注释):
$ ./test 10 : 20 20 : 10 20 : 10 20 : 20 5 : 20 20 : 5
同时
Lambda
表达式可不使用auto
接收并直接使用,如:int main() {int a = 10;int b = 20;[=]() { cout << a << " : " << b << endl; }(); /* 实例化匿名函数对象后直接使用()进行调用 */int i = [=]() { cout << a << " : " << b << endl;return 10; }(); /* 也可在实例化匿名函数对象使用()调用后对其返回值进行接收 */return 0; }
Lambda
表达式的捕获列表[]
只可捕获其父作用域中的变量;
-
Lambda
表达式之间不可相互赋值Lambda
表达式之间不可相互赋值,本质原因是虽然其为一个匿名函数对象,但其在底层有其相应的命名方式,每个Lambda
表达式的命名在底层是不同的,故其类型也不相同,无法相互赋值;
function 包装器
std::function
函数包装器是C++11引入的一个用于存储,复制和调用任何可调用目标的一个包装器;
其可以存储,复制和调用的对象包括:
- 普通函数
- 函数指针
-
Lambda
表达式 - 绑定表达式
- 函数对象(实现了
operator()
的类的对象,即仿函数)
基本语法为:
std::function<返回类型(参数类型列表)> 函数对象名;
以下列代码为例:
#include <iostream>
#include <functional>
using namespace std;// 定义一个普通的全局函数
void Print() { cout << "Hello Print" << endl; }// 定义一个函数对象(仿函数)类
class Functor {public:// 重载 operator() 使得该类的对象可以像函数一样被调用void operator()() { cout << "Hello Functor" << endl; }
};int main() {// 定义一个 lambda 表达式auto func1 = []() { cout << "Hello Func1" << endl; };// 使用 std::function 创建函数包装器// f1 包装普通函数 Printfunction<void()> f1 = Print;// f2 包装 Functor 类的实例(函数对象)function<void()> f2 = Functor();// f3 包装 lambda 表达式 func1function<void()> f3 = func1;// 通过统一的接口调用这些不同类型的可调用对象f1(); // 输出: Hello Printf2(); // 输出: Hello Functorf3(); // 输出: Hello Func1return 0;
}
其运行结果为:
$ ./test
Hello Print
Hello Functor
Hello Func1
function 包装器对成员函数的包装
function
包装器对成员函数的包装与普通函数的包装不同;
成员函数隐含了一个this
指针,通常在使用function
包装器对成员函数包装时通常要为成员函数加上 域作用限定符::
与 &
;
-
对静态成员函数
使用
function
包装器对静态成员函数而言其不存在对应的this
指针,可直接进行包装;但为了与普通函数或
Lambda
表达式的包装进行区分也应使用&
进行区分;#include <iostream> #include <functional>// 定义一个测试类 TestClass class TestClass {public:// 定义一个静态成员函数 func1// 静态成员函数不需要类的实例就可以调用// 参数: 两个整数 x 和 y// 返回值: 整数 (x + y)static int func1(int x, int y) {printf("func 1 ,x: %d , y: %d \n", x, y); // 打印输入的参数return x + y; // 返回两个参数的和} };int main() {// 创建一个 std::function 对象 f1// 它可以存储任何返回 int 并接受两个 int 参数的可调用对象// 这里我们将静态成员函数 TestClass::func1 的地址赋值给 f1std::function<int(int, int)> f1 = &TestClass::func1;// 调用 f1,传入参数 10 和 20// f1 会调用 TestClass::func1(10, 20)// 然后打印返回值std::cout << f1(10, 20) << std::endl;return 0; // 程序正常结束 }
-
对非静态成员函数的包装
非静态成员函数,即普通成员函数中默认存在一个隐含的
this
指针;对于静态成员函数的包装而言相同,都需要加上
&
与域作用限定符::
;同时其需要传入一个该类类型的指针,否则参数将不匹配;
#include <iostream> #include <functional>// 定义一个测试类 TestClass class TestClass {public:// 定义一个非静态成员函数 func2// 非静态成员函数需要通过类的实例来调用// 参数: 两个双精度浮点数 x 和 y// 返回值: 双精度浮点数 (x + y)double func2(double x, double y) {printf("func 2 ,x: %.1f , y: %.1f \n", x, y); // 打印输入的参数,保留一位小数return x + y; // 返回两个参数的和} };int main() {// 创建一个 std::function 对象 f2// 它可以存储任何返回 double 并接受 TestClass* 和两个 double 参数的可调用对象// 这里我们将非静态成员函数 TestClass::func2 的地址赋值给 f2std::function<double(TestClass*, double, double)> f2 = &TestClass::func2;// 创建 TestClass 的一个实例 tTestClass t;// 调用 f2,传入 &t(TestClass 实例的地址)和参数 1.1, 2.2// f2 会调用 t.func2(1.1, 2.2)// 然后打印返回值std::cout << f2(&t, 1.1, 2.2) << std::endl;return 0; // 程序正常结束 }
此处不能直接实例化匿名对象进行传入,匿名对象是一个临时对象,为右值,右值无法被取地址
&
;
两段代码结合测试并运行:
#include <iostream>
#include <functional>// 定义一个测试类 TestClass
class TestClass {public:// 定义一个静态成员函数 func1// 静态成员函数不需要类的实例就可以调用// 参数: 两个整数 x 和 y// 返回值: 整数 (x + y)static int func1(int x, int y) {printf("func 1 ,x: %d , y: %d \n", x, y); // 打印输入的参数return x + y; // 返回两个参数的和}// 定义一个非静态成员函数 func2// 非静态成员函数需要通过类的实例来调用// 参数: 两个双精度浮点数 x 和 y// 返回值: 双精度浮点数 (x + y)double func2(double x, double y) {printf("func 2 ,x: %.1f , y: %.1f \n", x, y); // 打印输入的参数,保留一位小数return x + y; // 返回两个参数的和}
};int main() {// 创建一个 std::function 对象 f1// 它可以存储任何返回 int 并接受两个 int 参数的可调用对象// 这里我们将静态成员函数 TestClass::func1 的地址赋值给 f1std::function<int(int, int)> f1 = &TestClass::func1;// 调用 f1,传入参数 10 和 20// f1 会调用 TestClass::func1(10, 20)// 然后打印返回值std::cout << f1(10, 20) << std::endl;// 创建一个 std::function 对象 f2// 它可以存储任何返回 double 并接受 TestClass* 和两个 double 参数的可调用对象// 这里我们将非静态成员函数 TestClass::func2 的地址赋值给 f2std::function<double(TestClass*, double, double)> f2 = &TestClass::func2;// 创建 TestClass 的一个实例 tTestClass t;// 调用 f2,传入 &t(TestClass 实例的地址)和参数 1.1, 2.2// f2 会调用 t.func2(1.1, 2.2)// 然后打印返回值std::cout << f2(&t, 1.1, 2.2) << std::endl;return 0; // 程序正常结束
}
运行结果为:
$ ./test
func 2 ,x: 10 , y: 20
30
func 1 ,x: 1.1 , y: 2.2
3.3
bind 绑定
bind
是C++11引入的一个函数模板;
该函数模板用于将函数和某些参数进行绑定创建一个新的可调用对象;
这个函数的对象可以不立即调用,同时其可以将部分或全部参数与函数进行绑定;
其基本语法为:
auto newCallable = std::bind(callable, arg1, arg2, ...);
-
callable
该参数为任何可调用对象,如函数指针,成员函数指针,
Lambda
表达式,function
包装后的可调用对象,仿函数等;
其可以绑定任意数量的参数,通常使用std::placeholders:: _1 , _2 , _3
来表示未绑定的参数;
同时bind
可改变原函数参数的顺序;
// 定义一个减法函数
int Sub(int a, int b) { return a - b; }// 定义一个除法结构体,使用函数调用运算符
struct Div {int operator()(int a, int b) { return a / b; }
};int main() {// 定义一个取模的 lambda 函数auto Mod = [](int a, int b) { return a % b; };// 绑定 Sub 函数,保持参数顺序不变auto sub1 = bind(Sub, placeholders::_1, placeholders::_2);cout << sub1(10, 20) << endl; // 输出 -10 (10 - 20)// 绑定 Sub 函数,交换参数顺序auto sub2 = bind(Sub, placeholders::_2, placeholders::_1);cout << sub2(10, 20) << endl; // 输出 10 (20 - 10)// 绑定 Div 函数对象,固定第二个参数为 2auto div1 = bind(Div(), placeholders::_1, 2);cout << div1(100) << endl; // 输出 50 (100 / 2)// 绑定 Div 函数对象,固定第一个参数为 10auto div2 = bind(Div(), 10, placeholders ::_1);cout << div2(2) << endl; // 输出 5 (10 / 2)// 绑定 Mod lambda 函数,固定两个参数auto mod1 = bind(Mod, 11, 10);cout << mod1() << endl; // 输出 1 (11 % 10)return 0;
}
在这个例子中:
-
sub1
简单绑定了一个函数,保持参数顺序不变;
-
sub2
绑定了一个函数,但交换了参数的顺序;
-
div1
绑定了一个函数对象(
Div
仿函数)并固定第二个参数; -
div2
绑定了一个仿函数并固定第一个参数;
-
mod1
绑定了一个
Lambda
表达式实例化的函数,并固定所有参数;
运行结果如下:
$ ./test
-10
10
50
5
1
在使用function
包装器对类内非静态成员函数包装时需要传入一个该类类型的指针类型(隐含this
指针);
同时在调用该function
包装后的非静态成员函数时需要传入一个对应的该类对象指针,在这种情况下可使用bind
绑定第一个this
指针从而减少调用参数;
class TestClass {public:// 定义一个成员函数,接受两个double参数并返回它们的和double func(double x, double y) {printf("func ,x: %.1f , y: %.1f \n", x, y);return x + y;}
};int main() {// 声明一个 std::function 对象 f2// 它表示一个接受 TestClass* 和两个 double 参数,返回 double 的函数// 这里绑定了 TestClass::func 成员函数function<double(TestClass*, double, double)> f2 = &TestClass::func;// 创建 TestClass 的实例TestClass t;// 使用 std::bind 创建一个新的可调用对象 testfunc// 绑定 f2(即 TestClass::func)到 TestClass 实例 t// placeholders::_1 和 _2 表示 testfunc 将接受两个参数auto testfunc = bind(f2, &t, placeholders::_1, placeholders::_2);// 调用 testfunc,传入 1.1 和 2.2 作为参数// 这相当于调用 t.func(1.1, 2.2)cout << testfunc(1.1, 2.2) << endl;return 0;
}
在这个例子中:
function<double(TestClass*, double, double)> f2 = &TestClass::func;
创建了一个function
对象以存储TestClass::func
的指针;
其中第一个参数必须为TestClass*
(该类的this
指针类型);
auto testfunc = bind(f2, &t, placeholders::_1, placeholders::_2);
使用了bind
创建一个新的可调用对象名为testfunc
;
并创建了一个TestClass
对象t
,并将该对象取地址&
绑定在新的可调用对象testfunc
的第一个参数上;
默认placeholders::_1, placeholders::_2
传递两个参数;
最后调用绑定后的函数并传入1.1
与2.2
作为参数;
执行结果为:
$ ./test
func ,x: 1.1 , y: 2.2
3.3