文章目录
- 一、可变参数模板
- 1.1 参数包的概念
- 1.2 参数包的展开
- 1.3 emplace系列
- 二、lambda表达式
- 2.1 lambda的格式
- 2.2 捕捉列表
- 2.3 lambda的原理
- 2.4 lambda的优势
- 三、函数包装器
- 3.1 function
- 3.2 bind
一、可变参数模板
C++11更新后,可以创建接受可变参数的函数模板和类模板。
1.1 参数包的概念
以下是基本可变参数的函数模板:
template <class... Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
void test()
{
ShowList();
ShowList(1);
ShowList(1, 2.4);
ShowList(1, 2.4, 'g');
ShowList(1, 2.4, 'g', 3.56);
}
- Args是一个模板参数包,args是一个函数参数包
- 参数包中可以包含0到任意个模板参数
- sizeof…运算符可以获取可变参数模板中参数的数量
ps:参数前面有省略号,就是一个可变模版参数,我们把带省略号的参数称为“参数包”。
ps:对于可变参数模板,编译器会从函数的实参推断模板参数类型。同时,编译器还会推断包中参数的数目。
1.2 参数包的展开
可变模版参数的一个主要特点,便是无法直接获取参数包中的每个参数,所以只能通过展开参数包的方式来获取参数包中的每个参数。
递归方式:
// 递归终止函数
void _ShowList()
{
cout << endl;
}
// 展开函数
template <class T, class... Args>
void _ShowList(const T& val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
template <class... Args>
void ShowList(Args... args)
{
_ShowList(args...);
}
利用子函数_ShowList每次获取参数包的第一个元素并打印,然后继续传递参数包,直到参数包没有参数,调用递归终止函数。
数组方式:
template<class T>
int PrintArgs(T val)
{
cout << val << " ";
return 0;
}
template<class... Args>
void ShowList(Args... args)
{
int arr[] = { PrintArgs(args)...};
cout << endl;
}
利用数组初始化的过程展开参数包并打印。
以下有一种更简洁的写法,运用了逗号表达式和折叠表达式(C++17):
template<class T>
void PrintArgs(T val)
{
cout << val << " ";
}
template<class... Args>
void ShowList(Args... args)
{
(PrintArgs(args), ...);
cout << endl;
}
1.3 emplace系列
STL容器中emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对于insert系列,emplace系列接口的优势到底在哪里呢?
接口展示:
//emplace系列
template <class... Args>
void emplace_back (Args&&... args);
//insert系列
template <class T>
void push_back (const T& val);
void push_back (T&& val);
单参数:
void test()
{
list<my::string> lt;//此处运用my::string,便于调试和观察
my::string s1 = "1111";
lt.push_back(s1);
lt.push_back(move(s1));
my::string s2 = "2222";
lt.emplace_back(s2);
lt.emplace_back(move(s2));
cout << endl;
lt.push_back("3333");
lt.emplace_back("3333");
}
其实,插入s1和s2的过程没有任何区别,有一个细微的区别在于直接插入"3333"时,push_back是构造+移动构造,而emplace_back是构造,相比之下只是少了一个移动构造,差别不大。
那么,为什么emplace_back是构造呢?因为参数包层层往下传递,直到节点的构造函数,在初始化列表中才解析出具体类型,所以就可以直接在节点上进行构造。
多参数:
void test()
{
list<pair<my::string, int>> lt;
lt.push_back(make_pair("1111", 1));
lt.push_back({ "2222",2 });
lt.emplace_back(make_pair("3333", 3));
lt.emplace_back("4444", 4);
}
因为emplace_back的形参是参数包,所以可以写成多参数的形式传入。
二、lambda表达式
2.1 lambda的格式
lambda表达式书写格式:
- [capture-list] (parameters) mutable -> return-type { statement }
- [capture-list] : 捕捉列表。捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。
- mutable:修饰符。mutable可以取消其常量性,让传值捕捉的变量(默认为const)可以被修改。
- ->returntype:返回值类型。
- {statement}:函数体。在该函数体内,可以使用参数和捕获的变量。
ps:若不需要传参,则可省略参数列表。
ps:使用mutable修饰,则不可省略参数列表。
ps:返回值类型明确时,可省略,编译器自动推导。
2.2 捕捉列表
捕捉方式分两种,传值捕捉和传引用捕捉。
传值捕捉:
void test()
{
int x = 1, y = 2;
//[var]
auto f1 = [x, y] {cout << x << " " << y << endl; };
//[=]
auto f2 = [=] {cout << x << " " << y << endl; };
}
class A
{
public:
void print()
{
//[this]
auto f3 = [this] {cout << _a1 << " " << _a2 << endl; };
}
private:
int _a1, _a2;
};
- [var]:传值捕捉var变量
- [=]:传值捕捉所有变量(包括this)
- [this]:传值捕捉this指针
传引用捕捉:
void test()
{
int x = 1, y = 2;
//[&var]
auto f1 = [&x, &y] {cout << x << " " << y << endl; };
//[&]
auto f2 = [&] {cout << x << " " << y << endl; };
}
- [&var]:传引用捕捉var变量
- [&]:传引用捕捉所有变量(包括this)
ps:可以混合捕捉,但不能重复捕捉。
ps:只能捕捉父作用域中的局部变量(父作用域,指包含lambda的语句块)。
2.3 lambda的原理
lambda表达式,底层原理就是仿函数(类似于范围for的底层是迭代器)。
先看看以下代码:
void test()
{
auto f1 = [](int x) {cout << x << endl; };
f1(1);
cout << typeid(f1).name() << endl;
auto f2 = [](int x) {cout << x << endl; };
f2(2);
cout << typeid(f2).name() << endl;
}
对于用户,lambda是匿名函数对象,所以用auto接收。但是即使定义完全相同的两个lambda,其函数类型还是不同。所以 lambda表达式之间不能相互赋值,它们之间的类型是互不相同的。
再看看以下代码:
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
void test()
{
double rate = 0.49;
// 函数对象
Rate r1(rate);
r1(10000, 2);
// lambda
auto r2 = [=](double monty, int year)->double
{return monty * rate * year;};
r2(10000, 2);
}
使用方式相同:
- 函数对象将rate作为其成员变量,在定义对象时给出初始值即可。
- lambda表达式通过捕获列表可以直接捕获到该变量。
底层实现相同:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
2.4 lambda的优势
对于一个商品类Goods:
struct Goods
{
string _name;
double _price;
int _evaluate;
Goods(const char* str, double price, int evaluate)
: _name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
我们想按不同方式取比较,进行排序。
仿函数:
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
void test()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
}
lambda表达式:
void test()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; });
}
仿函数和lambda表达式比较:
- 仿函数的方式比较笨重,且类名必须取的清晰,否则容易混淆不清。
- lambda表达式则简洁清晰,在原本传参的位置定义,可以清楚知道比较的方式。
三、函数包装器
目前,我们学习了三种回调函数的方式:函数指针、仿函数和lambda表达式。而在传参时,函数模板会因不同的类型而产生多份实例化,导致效率低下。所以,我们希望有统一的类型可以接收可调用对象,以达到包装的目的,提高效率。
3.1 function
function是一个模板类,提供了一种通用的、多态的函数封装,是一个函数包装器(适配器)。
template <class T> function; // undefined
template <class Ret, class... Args> class function<Ret(Args...)>;
ps:Ret是被调用函数的返回类型,Args是被调用函数的参数类型。
请看看以下代码:
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
struct SwapFunctor
{
void operator()(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
};
void test()
{
//函数指针
function<void(int&, int&)> f1 = Swap;
//函数对象
function<void(int&, int&)> f2 = SwapFunctor();
//lambda表达式
function<void(int&, int&)> f3 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
}
function提供了统一的类型来接收不同类型的可调用对象,包括函数指针、仿函数和lambda表达式等,实现了函数包装。
比较特殊的,是function接收类的成员函数:
class Plus
{
public:
static int plusi(int x, int y)
{
return x + y;
}
double plusd(double x, double y)
{
return x + y;
}
};
int main()
{
//类的静态成员函数
function<int(int, int)> f1 = Plus::plusi;
//类的普通成员函数
function<double(Plus*, double, double)> f2 = &Plus::plusd;
Plus p;
f2(&p, 1.1, 2.2);
function<double(Plus, double, double)> f3 = &Plus::plusd;
f3(Plus(), 1.1, 2.2);
return 0;
}
- function接收类的成员函数,要& + 类域(静态成员函数不用&)。
- 因为类的成员函数有隐含的参数this指针,所以function内部的类型要加上类指针。
- 由于传入指针比较麻烦,所以编译器做了特殊处理,可以传入类。
leetcode 150.逆波兰表达式求值
逆波兰表达式(function化简版):
class Solution
{
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;
unordered_map<string, function<int(int, int)>> hash =
{
{"+", [](int x, int y){return x + y;}},
{"-", [](int x, int y){return x - y;}},
{"*", [](int x, int y){return x * y;}},
{"/", [](int x, int y){return x / y;}}
};
for(auto& str : tokens)
{
if(hash.count(str))
{
int right = st.top();st.pop();
int left = st.top();st.pop();
st.push(hash[str](left, right));
}
else st.push(stoi(str));
}
return st.top();
}
};
3.2 bind
bind是一个模板函数,可以接收一个可调用对象,进行函数参数绑定,返回一个绑定后的对象。
//simple(1)
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
//with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
ps:fn是可调用对象,args是需要进行绑定的参数列表。
bind的绑定方式分两种,绑定值和绑定占位符。
int Sub(int x, int y)
{
return x - y;
}
void test()
{
function<int(int, int)> f1 = Sub;
f1(10, 5);
//绑定值——调整参数个数
function<int()> f2 = bind(Sub, 20, 10);
f2();
//绑定占位符——调整参数顺序
function<int(int, int)> f3 = bind(Sub, placeholders::_2, placeholders::_1);
f3(10, 5);
//同时绑定值和占位符
function<int(int)> f4 = bind(Sub, 20, placeholders::_1);
f4(10);
}
- placeholders是与bind一起使用的工具,用于指定绑定表达式中的占位符。
- _1代表函数调用实参的第一个位置,依此类推至 _n。
- 每次函数调用时,传入的实参会对应到bind的参数列表,再传入调用的函数。
- 绑定值后,function内的类型要相应的变化(参数类型个数要减少)。
对于之前function接收类的成员函数,我们可以用bind进行化简:
class Plus
{
public:
double plusd(double x, double y)
{
return x + y;
}
};
void test()
{
//function<double(Plus, double, double)> f3 = &Plus::plusd;
//f3(Plus(), 1.1, 2.2);
function<double(double, double)> f3 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
f3(1.1, 2.2);
}
运用bind将Plus()参数固定绑死,这样在传参时就不用每次传入,变得更加简洁。