目录
仿函数
产生原因
如果我们希望统计出一个数组中大于它首元素的数字的数量,并在一定程度上进行解耦:
#include <iostream>
using namespace std;
int RecallFunc(int *start, int *end, bool (*pf)(int)) {
int count=0;
for(int *i = start; i != end+1; i++) {
count = pf(*i) ? count+1 : count;
}
return count;
}
bool IsGreaterThanTen(int num) {
return num>10 ? true : false;
}
int main() {
int a[5] = {10,100,11,5,19};
int result = RecallFunc(a, a+4, IsGreaterThanTen);
cout<<result<<endl;
return 0;
}
这里我们使用函数指针将每次参与判断的两个数据与判断逻辑分离,但是如果我们想要直接将基值(10)也传入判断逻辑,那么IsGreaterThanTen就变成了:
bool IsGreaterThanTen(int num, int threshold)
{
return num>threshold ? true : false;
}
而 RecallFunc中函数指针类型是bool (*pf)(int)想要调用新的IsGreaterThanTen函数需要的函数指针是bool (*pf)(int,int),即要调用新的IsGreaterThanTen函数需要更改RecallFunc中的函数指针类型,这样就显得很麻烦,而且如果将基值设定为全局变量也不是明智之举因为这有可能会导致命名空间污染的问题出现,更改后的代码为:
#include <iostream>
using namespace std;
int RecallFunc(int *start, int *end, bool (*pf)(int, int), int threshold) {
int count = 0;
for (int *i = start; i != end + 1; i++) {
count = pf(*i, threshold) ? count + 1 : count;
}
return count;
}
bool IsGreaterThanTen(int num, int threshold) {
return num > threshold ? true : false;
}
int main() {
int a[5] = {10, 100, 11, 5, 19};
int threshold = 10; // 设置阈值
int result = RecallFunc(a, a + 4, IsGreaterThanTen, threshold);
cout << result << endl;
return 0;
}
仿函数的定义
1、仿函数(Functor)又称为函数对象(Function Object)是一个能行使函数功能的类
2、每个作为仿函数的类,都必须重载一个或多个函数调用运算符(),这能使得我们几乎可以像调用普通函数那样调用仿函数
3、调用仿函数,实际上就是通过类对象调用重载后的(),类对象可以有名也可以是匿名
补充:如果编程者要将某种“操作”当做算法的参数,一般有两种方法:
- 将该“操作”设计为一个函数,再将函数指针当做算法的一个参数。
- 将该“操作”设计为一个仿函数,再实例化它的对象,并以此对象作为算法的一个参数
很明显第二种方法会更优秀,因为第一种方法扩展性较差,当函数参数有所变化,则无法兼容旧的代码,因此为了减少对源代码的更改以及使得源代码可以被多次复用,我们就应该使用仿函数。
写一个仿函数类,除了维护类的基本成员函数外,只需要重载 ()运算符 。这样既可以免去对一些公共变量的维护,也可以使重复使用的代码独立出来,以便下次复用。而且相对于函数更优秀的性质,仿函数还可以进行依赖、组合与继承等,这样有利于资源的管理
STL 中的容器 set 就使用了仿函数 less ,而 less 继承的 binary_function,就可以看作是对于一类函数的总体声明,这是函数做不到的:
// less的定义 template<typename _Tp> struct less : public binary_function<_Tp, _Tp, bool> { bool operator()(const _Tp& __x, const _Tp& __y) const { return __x < __y; } }; // set 的申明 template<typename _Key, typename _Compare = std::less<_Key>,typename _Alloc = std::allocator<_Key>> class set;
C++ binary_function与unary_function使用详解_c++ unary functuon-CSDN博客
例一
使用仿函数解决上述仅使用函数指针无法解决的问题:
#include <iostream>
using namespace std;
//大于基值得判断逻辑
class IsGreaterThanThresholdFunctor {
public:
explicit IsGreaterThanThresholdFunctor(int t):threshold(t){}
bool operator() (int num) const {
return num > threshold ? true : false;
}
private:
const int threshold;
};
//RecallFunc的第三个参数是选用得判断逻辑
int RecallFunc(int *start, int *end, IsGreaterThanThresholdFunctor m) {
int count = 0;
for (int *i = start; i != end + 1; i++) {
count = m(*i) ? count + 1 : count;
}
return count;
}
int main() {
int a[5] = {10,100,11,5,19};
int result = RecallFunc(a, a + 4, IsGreaterThanThresholdFunctor(10));//实例化一个IsGreaterThanThresholdFunctor仿函数类的匿名对象,同时该类中的基值为10
//也可以实例化一个有名对象,然后传递该有名对象
//IsGreaterThanThresholdFunctor myFunctor(10);//实例化有名对象
//int result = RecallFunc(a, a + 4, myFunctor);//将实例化后的对象传递给RecallFunc函数
cout << result << endl;
}
例二
仿函数类可以和函数模板进行搭配从而使得重载的()可以支持任意类型:
#include <iostream>
#include <typeinfo>
using namespace std;
// 仿函数定义
class Add {
public:
Add(int n) : num(n) {}
// 模板化的 operator(),支持任意类型
template<typename T>
T operator()(T x) const
{
return x + num;
}
// 支持两个参数的模板化 operator()
template<typename T, typename U>
auto operator()(T x, U y) const
{
return x + y + num;
}
private:
int num;
};
int main() {
//这里的5是为了实例化仿函数类对象add
Add add(5);//实例化一个名为add的仿函数类的对象,且该对象中的num为5(5就是基值)
//传递一个参数是为了使用仿函数类中的()重载函数(由编译器找到并调用最合适的重载函数)
std::cout << add(10) << std::endl; // 输出:15
// 传递两个参数
std::cout << add(10, 20.5) << std::endl; // 输出:35.5
// 传递浮点数参数
std::cout << add(10.5) << std::endl; // 输出:15.5
return 0;
}
结论:
1、仿函数一般是用于处理那种简单逻辑且需要多次复用的代码
2、仿函数类中可以不提供构造函数,只需要重载()即可
lmabda表达式
格式:
[capture list] (params list) mutable -> return type { function body }
- capture list:捕捉列表,用于捕获当前作用域的中变量供lambda表达式使用,编译器会根据[]判断接下来的代码是不是lambda函数
- params list:形参列表,与普通函数的参数列表一致,若不需要传参可以连带()一起省略
- mutable :决定是否可以修改捕获到的变量,默认情况下不能修改捕获到的参数(const在()右侧),mutable关键字使得可以修改捕获到变量,使用该修饰符时,参数列表即使为空也不可省略
- return_type:返回值类型,可以省略,编译器会依据以下规则对返回值类型进行自动推导
1、若函数体中已有return,则该Lambda表达式的返回类型由return语句的返回类型确定2、若函数体中没有return,则返回值为void类型
- function body:函数体,在该函数体内除了可以使用参数列表中的参数外,还可以使用所有捕获到的变量
补充:有三种类型的不完整lambda表达式:
序号 格式 1 [capture list] (params list) -> return type {function body} 2 [capture list] (params list) {function body} 3 [capture list] {function body}
- 1号:省略mutable,表达式不能修改捕获到的变量
- 2号:省略mutable和return type,不能修改 + 由编译器推导表达式返回值类型
- 3号:省略mutable和return type和params list,再加一个不需要形参
调用方式:
// 定义一个lambda表达式
auto add = [](int x, int y) { return x + y; };
// 调用lambda表达式
int result = add(3, 5);
std::cout << result << std::endl; // 输出:8
常见使用案例:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//比较函数
bool cmp(int a, int b)
{
return a < b;
}
int main()
{
vector<int> myvec{ 3, 2, 5, 7, 3, 2 };
vector<int> lbvec(myvec);
sort(myvec.begin(), myvec.end(), cmp); // 旧式做法
cout << "predicate function:" << endl;
for (int it : myvec)
cout << it << ' ';
cout << endl;
//使用lambda表达式不需要cmp函数
sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; });
cout << "lambda expression:" << endl;
for (int it : lbvec)
cout << it << ' ';
}
- 在C++11前,我们使用STL的sort函数,需要提供一个有名函数。而有了Lambda表达式后,我们只需要传入一个lambda表达式即可(相当于一个用于比较的匿名对象),方便简洁,代码可读性增强
捕获外部变量
基本概念:在Lambda表达式内部可以使用表达式当前所在作用域的变量,但必须利用[ ]指明该变量,这一过程也称Lambda表达式“捕获”了外部变量,捕获方式分为值捕获、引用捕获和隐式捕获
注意事项:向lambda表达式的传参不等于捕获变量,且由于lambda表达式的类型过于复杂所以lambda表达式的类型都是由auto自动推导的(不信自己typeid(f).name()去试)
#include <iostream>
using namespace std;
int main()
{
int a = 123;
auto f = [a] { cout << a << endl; };
f(); //lambda表达式使用的参数是捕获到的a
//向lambda表达式传参
auto x = [](int a) {cout << "a = " << a << endl; };
x(123);//lambda表达式使用的参数是自主传递的参数123
}
值捕获
基本概念:与值传递类似,被捕获的变量的值在Lambda表达式创建时通过值拷贝的方式传入
注意事项:
1、在Lambda表达式函数体中不能修改捕获到的该外部变量的值
2、在表达式外对该变量的修改不会影响影响Lambda表达式中的值
int main()
{
int a = 123;
auto f = [a] { cout << a << endl; };
a = 321;
f(); // 输出:123
}
引用捕获
基本概念:与传引用类似,就是在被捕获的变量前加一个&
注意事项:
1、在Lambda表达式函数体中能修改捕获到的该外部变量的值
2、在表达式外对该变量的修改会影响影响Lambda表达式中的值
int main()
{
int a = 123;
auto f = [&a] {a = 2; cout << a << endl; };
f(); // 输出:2、不是123
}
int main()
{
int a = 123;
auto f = [&a] { cout << a << endl; };
a = 552;
f(); // 输出:552、不是123
}
隐式捕获
基本概念:值捕获和引用捕获都需要我们在捕获列表中显示列出Lambda表达式中使用的外部变量。除此之外,我们还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获
捕获方式:[=] 和 [&]
- [=]:表示以值捕获的方式捕获外部变量
int main()
{
int a = 123;
auto f = [=] { cout << a << endl; }; // 值捕获
f(); // 输出:123
}
- [&]:表示以引用捕获的方式捕获外部变量
int main()
{
int a = 123;
auto f = [&] { cout << a << endl; }; // 引用捕获
a = 321;
f(); // 输出:321
}
混合捕获
基本概念:Lambda表达式还支持以混合的方式捕获外部变量,即对多种捕获方式进行组合使用
[=, &x] | 变量x以引用形式捕获,其余变量以传值形式捕获 |
[&, x] | 变量x以值的形式捕获,其余变量以引用形式捕获 |
... | ... |
注意事项:
1、捕捉列表不允许重复传递,否则编译报错
int main()
{
int a = 123;
auto f = [=,a] { cout << a << endl; };
a = 552;
f(); // 输出:552、不是123
}
总结
捕获形式 | 说明 |
[] | 不捕获任何外部变量 |
[变量名, …] | 默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符) |
[this] | 以值的形式捕获this指针(用于访问成员函数) |
[=] | 以值的形式捕获所有外部变量 |
[&] | 以引用形式捕获所有外部变量 |
[=, &x] | 变量x以引用形式捕获,其余变量以传值形式捕获 |
[&, x] | 变量x以值的形式捕获,其余变量以引用形式捕获 |
参数列表
基本概念:Lambda表达式的参数和普通函数的参数类似,但是在Lambda表达式中传递参数还有一些限制:
- 参数列表中不能有默认参数
- 不支持可变参数
- 所有参数必须有参数名
嵌套lambda表达式
//嵌套lambda表达式
#include <iostream>
int main() {
// 嵌套使用Lambda表达式
auto outerLambda = [](int x) {
// 内部Lambda表达式
auto innerLambda = [](int y) {
return y * 2;
};
int result = innerLambda(x);
return result;
};
int num = 5;
int nestedResult = outerLambda(num);
std::cout << "Nested Lambda Result: " << nestedResult << std::endl;//输出:10
return 0;
}
闭包
基本概念:底层上,当编译器遇到lambda表达式时,它会将其转换为一个匿名的仿函数类(也称为闭包类)该仿函数类具有与lambda表达式相同的行为(闭包类中会包含Lambda表达式的函数调用运算符()
的重载)如果lambda表达式捕获了一些变量,那么这些变量也会作为闭包类的成员变量保存下来
下方的lambda表达式在底层会形成一个对应的名为__lambda_12345的闭包类:
#include <iostream>
int main() {
int x = 10;
int y = 5;
auto lambda = [x, y] () {
return x + y;
};
std::cout << "Result: " << lambda() << std::endl;
return 0;
}
class __lambda_12345 {
private:
int x;
int y;
public:
__lambda_12345(int x, int y) : x(x), y(y) {}
int operator()() {
return x + y;
}
};
函数对象与lambda表达式
函数对象就是仿函数,即一个重载了()的类对象,从上面的描述我们可以发现调用lambda表达式生成的闭包类与仿函数一样,都重载了(),那么仿函数与labbda到底有何区别呢?下面我们实现了一个函数对象和一个仿函数,并查看它们的反汇编代码:
#include <iostream>
using namespace std;
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
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);
return 0;
}
结论:与仿函数显示调用()重载不同,lambda表达式是隐式的调用了()重载函数, lambda表达式相当于对仿函数的封装
lambda表达式的编号与注意事项
基本概念:两个看起来一样的lambda表达式的类型是不同的,每个lambda表达式都有其特殊的编号(由极小的概率会重复),新版编译器会将这个编号进行优化
#include <iostream>
using namespace std;
int main()
{
auto f1 = [] {cout << "hello world" << endl; };
auto f2 = [] {cout << "hello world" << endl; };
f1();
f2();
return 0;
}
结论:这也是为什么两个lambda表达式间不能相互赋,虽然两个lambda表达式看起来一样,但实际上它们在汇编层面的编号不一样
补充:
1、虽然两个lambda表达式间不能相互赋值,但是他们允许使用一个lambda表达式赋值构造一个新的lambda表达式
#include <iostream>
using namespace std;
int main()
{
auto f1 = [] {cout << "hello world" << endl; };
auto f2 = f1; // 正确,因为 `f2` 使用 `auto` 推导为 `f1` 的类型
f1();
f2();
return 0;
}
2、拷贝构造一个新的也可以,还可以将lambda表达式赋值给相同类型的函数指针
#include <iostream>
using namespace std;
void (*PF)();
int main()
{
auto f2 = [] {cout << "hello world" << endl; };
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
function包装器
基本概念:也叫作适配器,它的本质是一个类模板
包含头文件:
<functional>
模板原型:
template <class Ret, class... Args>
class function<Ret(Args...)>;
- Ret:被调用函数或对象的返回类型
- Args…:被调用函数或对象的形参
使用方式: function<返回值类型(参数1,参数2,....)> 包装器类对象名 = 要封装的类型
- 要封装的类型可以是普通函数、函数对象、lambda表达式等
存在原因:不同类型会实例化多个函数模板,封装这些类型使得只实例化一个函数模板
未使用function前:
#include <iostream>
using namespace std;
//函数模板
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;//打印count当前所处的地址,可以判断是否处于不同的函数模板中
return f(x);//最后调用f(x),并返回其结果
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
使用function后:
#include<functional>
#include <iostream>
using namespace std;
template<class F, class T>
T useF(F f, T x)//函数或对象名,传入的参数
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数指针
function<double(double)> fc1 = f;
fc1(11.11);
cout << useF(fc1, 11.11) << endl;
// 函数对象
function<double(double)> fc2 = Functor();
fc2(11.11);
cout << useF(fc2, 11.11) << endl;
// lambda表达式
function<double(double)> fc3 = [](double d)->double { return d / 4; };
fc3(11.11);
cout << useF(fc3, 11.11) << endl;
return 0;
}
其它妙用
使得解决leetcode中的求解逆波兰表达式更加简单
思路:如果传递函数指针类型或者函数对象类型,那么就还需要在外面写四个函数或者重载(),所以我们选择使用lambda表达式,而lambda表达式没有明确的类型,所以我们可以使用function对其进行封装
获取静态成员函数类型时的注意事项
#include <iostream>
#include<functional>
using namespace std;
class Plus
{
public:
//静态成员函数
static int plusi(int a, int b)
{
return a + b;
}
//非静态成员函数
double plusd(double a, double b)
{
return a + b;
}
};
//普通函数
int f(int a, int b)
{
return a + b;
}
int main()
{
// 普通函数
function<int(int, int)> fc1 = f;
cout << fc1(1, 1) << endl;
// 静态成员函数
function<int(int, int)> fc2 = &Plus::plusi;
cout << fc2(1, 1) << endl;
// 非静态成员函数
// 非静态成员函数需要对象的指针或者对象去进行调用
/*Plus plus;
function<double(Plus*, double, double)> fc3 = &Plus::plusd;
cout << fc3(&plus, 1, 1) << endl;*/
//对于非静态成员函数使用这种方法也行
function<double(Plus, double, double)> fc3 = &Plus::plusd;
cout << fc3(Plus(), 1, 1) << endl;
return 0;
}
偏函数
基本概念:将一个多参数函数的一部分参数绑定固定值,从而生成一个新的函数,该函数只需要其余的参数即可调用,它的本质是函数模板
实现方式:
C++11中,实现偏函数的一种常见方法是使用bind函数
包含头文件:
<functional>
格式:
auto new_function = std::bind(function, arg1, arg2, ...);
function:
要绑定的函数或函数对象(仿函数)arg1, arg2, ...:
要绑定的参数
假设我们有一个普通的函数 add
,它接受两个整数参数并返回它们的和,现在我们使用bind
实现一个偏函数:
#include <iostream>
#include <functional> // std::bind, std::placeholders
// 普通函数
int add(int a, int b) {
return a + b;
}
int main() {
// 使用 std::bind 创建一个偏函数,将第一个参数固定为 10
auto add10 = std::bind(add, 10, std::placeholders::_1);
// 调用偏函数,只需传递一个参数
std::cout << "add10(5) = " << add10(5) << std::endl; // 输出:add10(5) = 15
// 你也可以绑定第二个参数
auto addTo5 = std::bind(add, std::placeholders::_1, 5);
std::cout << "addTo5(3) = " << addTo5(3) << std::endl; // 输出:addTo5(3) = 8
return 0;
}
创建偏函数:bind
将 add
函数的第一个参数固定为 10
,并且生成一个新的函数对象 add10
,这个函数对象只需要一个参数,std::placeholders::_1
表示这个参数在调用新函数时传递
auto add10 = std::bind(add, 10, std::placeholders::_1);
调用偏函数:调用偏函数 add10
,并传递一个参数 5
,相当于调用 add(10, 5)
,返回结果为 15
std::cout << "add10(5) = " << add10(5) << std::endl; // 输出:add10(5) = 15
绑定第二个参数:bind
将 add
函数的第二个参数固定为 5
,生成一个新的函数对象 addTo5
,这个函数对象只需要一个参数。调用 addTo5(3)
相当于调用 add(3, 5)
,返回结果为 8
auto addTo5 = std::bind(add, std::placeholders::_1, 5);
std::cout << "addTo5(3) = " << addTo5(3) << std::endl; // 输出:addTo5(3) = 8
注意事项:
1、placeholders::_1
、placeholders::_2
等参数是占位符,表示在调用新的函数对象时需要传递的参数位置(告诉编译器,如何填入参数)
2、bind
也可以用于绑定成员函数,绑定成员函数时需要提供对象实例或指向对象的指针
#include <iostream>
#include <functional> // std::bind, std::placeholders
class Adder {
public:
int add(int a, int b) {
return a + b;
}
};
int main() {
Adder adder;
// 使用 std::bind 绑定成员函数,将第一个参数固定为 10
auto add10 = std::bind(&Adder::add, adder, 10, std::placeholders::_1);
// 调用偏函数,只需传递一个参数
std::cout << "add10(5) = " << add10(5) << std::endl; // 输出:add10(5) = 15
return 0;
}
解释:我们定义了一个 Adder
类,并使用bind
绑定其成员函数 add
,生成一个新的函数对象 add10
(第二个参数adder
表示绑定的成员函数Adder::add
所属的对象或者对象指针)
,固定第一个参数为 10,
通过这种方式,可以灵活地将函数参数部分固定,从而简化函数调用和代码组织
~over~