文章目录
可变参数模板
C++11之前的函数模板和类模板中只能含有固定的参数数量,这使得我们在不确定参数个数的情况下,使用模板就会变得比较麻烦。而C++11提供了可变参数模板,允许模板能接受可变数量的模板参数,这大大提高了C++泛型编程的灵活性和通用性。下面介绍可变参数模板的原理和用法。
使用参数包
可变参数模板的核心思想是使用模板参数包和函数参数包。这些包可以包含0个或者是多个参数,通过展开来处理这些参数。其中模板参数包使用...
来定义(函数模板也是一样),比如:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
当我们使用参数包时,参数的顺序是按照传入时的顺序保持不变的。在展开参数包时,参数会按照它们在包中的顺序依次展开。
下面我们用简单的代码样例来证明:
递归函数方式展开参数包
来观察以下代码
#include<iostream>
using namespace std;
void print() {
cout << "end" << endl;
}
template <class T,class... Args>
void print(T first, Args... args)
{
cout << first << " ";
print(args...);
}
int main() {
print(1, 'a', 'b', 'c');
return 0;
}
运行结果:
- 为什么会出现这种结果
首次调用print函数时,print
函数依次接收参数1,’a‘,'b','c'
,此时参数包args
中包含的参数就是abc
,实参1
传递给了参数frist
,于是第一个打印出来的参数就是1
.接下来,把args
整个参数包再传给print
,按照顺序,依旧把第一个参数传递给first
,剩下的参数打包又传递给了args
。于是第二次print
打印出来的first
就是a
,同样的,后面依次打印bc
。那为什么还会打印end
呢?这是因为递归到最后,args
里没有参数了不会再调用模板实例化出来带参的print函数,此时print(args...);
调用的是无参版的print
函数,至此递归终止。这样我们就拿到了参数包中所有的参数。
- 为什么要设计一个无参的print函数?
这是因为递归到最后参数包里面会没有参数,此时模板无法再实例化出一个符合的函数,就需要我们提前设计一个无参版的print函数来终止递归。
逗号表达式展开参数包
观察下面代码:
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1, 'A', std::string("sort"));
return 0;
}
重点观察这一行代码:
int arr[] = { (PrintArg(args), 0)... };
(PrintArg(args), 0)
是一个逗号表达式,这个表达式的结果是0.通过这种方式,每个args里的参数都会传递给PrintArg
。更具体的,这行代码实际上会被转换成 ((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), ... )
, 之后arr
数组的元素都是0,元素个数是args
的参数个数。这样一来,PrintArg()
函数接收到了args
中的每一个参数并打印出来了。
对比insert和emplace系列接口
在STL容器中,insert系列其实就是我们常用的push_back这种,而emplace系列比如emplace_back,借助参数包和完美转发将元素在容器内直接构造,中间没有其它构造和拷贝。这使得empace_back会更加高效,尤其是当需要构造复杂对象时。为什么emplace_back可以直接构造,这其实就是用到了参数包的原理。
以push_back和empalce_back为例,假设容器的元素类型为pair,分别观察其使用区别:
- push_back:
vector<pair<int,int> v;
v.push_back({1,1});
这个过程会首先构造一个初始化列表对象,再转换并构造出一个pair匿名对象,最后将这个匿名对象传入push_back函数中完成插入元素。
- emplace_back
vector<pair<int,int> v;
v.emplace_back(1,1);
注意这里参数的区别,由于使用了可变参数模板,emplace_bake不需要先构造出一个pair匿名对象,而是通过展开参数包将参数包里的参数直接传递给pair的构造函数构造一个pair对象,省略了初始化列表构造pair。
这就是可变参数模板的一个常见的应用。
lamdba表达式
Lambda表达式是C++11引入的一种特性,允许你在代码中定义匿名函数(即没有名字的函数)。这种表达式可以捕获其上下文中的变量,并在需要时进行传递和调用。
lamdba表达式的基本语法
lamdba表达式语法格式如下:
[capture](parameters)mutable -> return_type {
// Function body
};
其中
capture
:捕捉列表,用于指定哪些外部变量可以被lamdba表达式获取,换句话来说,捕捉列表中的变量可以在函数体中使用。参数列表在[]中,编译器根据[]来判断接下来的代码是否为lambda函数parameters
:参数列表。和普通函数的参数一样。如果不需要参数传递,则可以连同()一起省略。- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。该关键字通常省略。
->return_type
:return_type是返回类型,可以连着->省略,编译器能够自动推导出来function body
:函数体,包含了lambda表达式要执行的代码。
来看下面的使用样例:
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[]{};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c){b = a + c; };
fun1(10);
cout<<a<<" "<<b<<endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int{return b += a+ c; };
cout<<fun2(10)<<endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
其中最常用的就是下面这种
auto fun2 = [=, &b](int c)->int{return b += a+ c; };
cout<<fun2(10)<<endl;
使用auto fun2
来让编译器自动推导出该函数对象的类型,在后面就像调用普通函数一样使用fun2
。于是我们很容易想到,lamdba表达式能帮助我们在一个函数里面定义函数。
关于捕捉列表
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]
:var表示外部的某个变量,表示传值方式捕捉变量var[=]
:表示传值方式捕捉父作用域中所有的变量包括this[&var]
:表示引用传递捕捉变量var,也就意味着在lamdba函数体中修改var也会影响到外部的var。[&]
:表示引用传递捕捉所有父作用域中的变量(包括this)[this]
:表示值传递方式捕捉当前的this指针
值得注意的是
-
父作用域指的是包含lambda函数的语句块 ,例如在man函数中定义一个lamdba,则该lamdba的父作用域就是main函数域。
-
语法上捕捉列表可由多个捕捉项组成,并以逗号分割。例如
[=, &a, &b]
表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。 -
捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:
[=, a]
:=已经以值传递方式捕捉了所有变量,捕捉a重复。这里要注意区分传值和传引用的区别。 -
lambda表达式之间不能相互赋值,即使看起来类型相同
函数对象与lamdba表达式
函数对象又被称为仿函数,借用重载()
符号来使得用起来就像函数一样。从使用方式上看,仿函数和lamdba表达式完全一样。其实,lamdba的底层也是利用了仿函数的处理方式,即如果定义了一个lamdba表达式,编译器会自动生成一个类,在该类中重载了operator()。
function包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器,用于存储、传递和调用可调用对象(如函数、lambda表达式、函数对象等)。
基本用法
语法格式如下:
function<返回类型(参数类型...)> fa;
fa就是一个包装器对象,存储了一个函数,函数的返回值类型和参数类型在 <> 中就已经声明了。
- 包装普通函数
#include <iostream>
#include <functional>
void printMessage(const std::string& message) {
std::cout << message << std::endl;
}
int main() {
std::function<void(const std::string&)> func = printMessage;
func("Hello, World!");
return 0;
}
- 包装lamdba表达式
这种比较常用
#include <iostream>
#include <functional>
int main() {
// 包装lambda表达式
std::function<int(int, int)> add = [](int a, int b) {
return a + b;
};
std::cout << "Sum: " << add(3, 4) << std::endl;
return 0;
}
- 包装仿函数
#include <iostream>
#include <functional>
// 函数对象
struct Multiply {
int operator()(int a, int b) const {
return a * b;
}
};
int main() {
// 包装函数对象
std::function<int(int, int)> multiply = Multiply();
std::cout << "Product: " << multiply(3, 4) << std::endl;
return 0;
}
- 包装成员函数
#include <iostream>
#include <functional>
class Printer {
public:
void print(const std::string& message) {
std::cout << message << std::endl;
}
};
int main() {
Printer printer;
// 包装成员函数
std::function<void(Printer&, const std::string&)> func = &Printer::print;
func(printer, "Hello, Member Function!");
return 0;
}
包装成员函数时需要注意,类中的非静态成员函数默认第一个参数类型是该类本身。
bind函数
bind顾名思义就是绑定的意思,这个函数对象可以将部分参数绑定到某个函数上,生成一个新的可调用对象来“适应”原对象的参数列。
比如我们用function对象存储了一个类的非静态成员函数,但是该函数每次调用都需要传递一个该类对象作为参数,我们其实可以在存储这个函数的时候绑定该类的对象参数,以后我们再拿function对象调用该函数时就不用再传递该类的对象作为参数了,编译器会帮你传。
用上面包装器包装成员函数的代码样例:
class Printer {
public:
void print(const std::string& message) {
std::cout << message << std::endl;
}
};
int main() {
Printer printer;
// 包装成员函数
std::function<void(const std::string&)> func = std::bind(&Printer::print, &printer,placeholders::_1);
func("Hello, Member Function!");
return 0;
}
-
创建 Printer 类的对象 printer。
-
使用 std::bind 将 Printer 类的 print 成员函数与 printer 对象绑定,并使用 std::placeholders::_1 占位符表示调用时需要传入的参数位置。
-
将绑定的成员函数存储在 std::function<void(const std::string&)> 类型的 func 变量中。