仿函数和std::function
常常用到各种形式的仿函数调用api,stl是怎么实现这些函数/类的
// sort, for_each的实现是怎样的std::sort(nums.begin(), nums.end(), [](int a, int b){return a > b;});
std::for_each(nums.begin(), nums.end(), [](int& item){});
// 需要定义Vec3的bool运算和std::hash<Vec3>()(const Vec3&)std:: nordered_map<Vec3, int, std::hash<Vec3>, std::equal_to<Vec3>> mp;
// 为什么这里的std::greater<int>可以写成std::greater<>std::priority_queue<int, std::vector<int>, std::greater<int>> q;
// 为什么这里既可以通过重载Vec3的operator<, 又可以通过创建一个重载了operator()(Vec3, Vec3)的cmp类, 但是不能把cmp写成lambdastd::priority_queue<Vec3> q;还有就是std::function是怎么对各种形式的仿函数进行封装的,又有什么优点?std::bind是干什么的?
函数指针的声明格式
typedef void(*AttackFunc)(Hero&), 星号和函数名作为一个整体加括号,前面写返回值,后面写参数。
函数作为一等公民可以方便的实现策略模式(Stragegy Pattern), 使得某个函数的具体实现方式可插拔,可写在其他地方,可写进配置文件等等。
struct Hero{ int hp = 10;};
typedef void (*AttackFunc)(Hero&);
void Tick(Hero& hero, AttackFunc func){ func(hero);}
void AttackSword(Hero& hero){ hero.hp -= 1;}
void AttackGun(Hero& hero){ hero.hp -= 5;}
int main(){ Hero hero; Tick(hero, AttackGun); Tick(hero, AttackSword);}这种方式有两个缺点:
- 无法传递额外参数(捕获局部变量)
- 得声明成全局函数,无法就地定义
c语言通过void*类型擦除可以解决第一个问题
void Tick(Hero& hero, AttackFunc func, int val){ func(hero, &val);}
void AttackSword(Hero& hero, void* value){ hero.hp -= *static_cast<int*>(value);}
void AttackGun(Hero& hero, void* value){ hero.hp -= *static_cast<int*>(value) * 2;}
int main(){ Hero hero; int BaseDamage = 1; Tick(hero, AttackGun, BaseDamage); Tick(hero, AttackSword, BaseDamage);}如果需要捕获更多的局部参数就得使用一个结构体,把void*转型成结构体指针
仿函数和lambda
Section titled “仿函数和lambda”首先我们知道functor和lambda本质上是一样的,用cppinsight可以看到编译器背后生成的代码(这个也可以用来调试模板元,神器!)
仿函数重载operator()是建议加const的,因为调用者一般喜欢const AttackFunc& func这种形式传入,只能执行const方法。
struct Hero{ int hp = 10;};
struct AttackFunctorSword{ void operator()(Hero& hero) const { hero.hp -= value; }; int value = 1;};
struct AttackFunctorGun{ void operator()(Hero& hero) const{ hero.hp -= value; }; int value = 1;};
template <typename AttackFunc>void Tick(Hero& hero, const AttackFunc& func){ func(hero);}
int main(){ Hero hero; int BaseDamage = 1;
AttackFunctorGun gunFunctor; gunFunctor.value = BaseDamage;
AttackFunctorSword swordFunctor; swordFunctor.value = BaseDamage;
Tick(hero, gunFunctor); Tick(hero, swordFunctor);}其实lambda里的operator()也是默认加const的, 因此对于需要修改变量值的情况有两种写法
// 1. 引用捕获int a = 1;auto f = [&a](){ a = 2;}
// 2. mutable , mutable会把默认加的const给去掉int a = 1;auto f [a]() mutable { a = 2;}std::function
Section titled “std::function”仿函数和模板的缺点在于,得用模板的形式来实现strategy这种静态多态。而模板的缺点:
- 不能做到头文件分离(即使分离也得显示实例化,非常滴麻烦)
- 我有n个具体的AttackFunctor就需要编译后实例化n次,导致内存占用增加
因此std::function横空出世
void Tick(Hero& hero, const std::function<void(Hero&)>& func){ func(hero);}int main(){ Hero hero; int BaseDamage = 1;
std::function<void(Hero&)> GunFunc = [BaseDamage](Hero& h){ h.hp -= BaseDamage * 2; }; std::function<void(Hero&)> SwordFunc = [BaseDamage](Hero& h){ h.hp -= BaseDamage; }; Tick(hero, GunFunc); Tick(hero, SwordFunc);}那么std::function是怎么实现可以同时warp functor、lambda和函数指针的呢 ==> 类型擦除
首先明确一下需要支持的功能
Function<int(int)> function_1([](int x){ return x + 1;});Function<int(int)> function_2 = &FuncPtr;int a = 1;std::cout << function(a) << function_2(a);实现一个初版
template<typename Ret, typename ... Args>struct Function<Ret(Args...)>{
};模板参数有了,但是怎么实现传入的函数参数呢,这个函数参数需要同时支持函数指针和带有operator()的结构体/lambda。既然要把这三种传入类型warp起来,自然就要有个warp类, 下面的这个FuncBase就是warp类,因为Warp类需要支持调用任意参数和返回值的operator(), 所以写在Function内部,可以使用Function的模板参数
template<typename Ret, typename ... Args>struct Function<Ret(Args...)>{ template<typename F> struct FunctionBase{ Ret Call(Args... args){ return std::invoke(m_f, std::forward<Args>(args)...); } F m_f; };
std::shared_ptr<FunctionBase> m_f;};这样显然过不了编译,因为FunctionBase依赖于模板参数F,因此我们需要把这个参数F擦除掉
template<typename Ret, typename ... Args>struct Function<Ret(Args...)>{
struct FunctionBase{ virtual Ret Call(Args... args) = 0; virtual ~FunctionBase() = default; };
template<typename F> struct FunctionImpl : public FunctionBase { Ret Call(Args... args) override { return std::invoke(m_f, std::forward<Args>(args)...); } F m_f; };
std::shared_ptr<FunctionBase> m_f;};通过一个不带模板参数的基类就可以实现类型的擦除,定义出这样的成员变量std::shared_ptr<FunctionBase> m_f;
最后补全拷贝构造
template<typename Ret, typename ... Args>struct Function<Ret(Args...)>{ struct FunctionBase{ virtual Ret Call(Args... args) = 0; virtual ~FunctionBase() = default; };
template<typename F> struct FunctionImpl : public FunctionBase { FunctionImpl(F f) :m_f(std::move(f)){}
Ret Call(Args... args) override { return std::invoke(m_f, std::forward<Args>(args)...); } F m_f; };
Function() = default;
template<typename F> Function(F f) : m_base(std::make_shared<FunctionImpl<F>>(std::move(f))) {}
// Function operator= (const Function&) = delete; Ret operator()(Args... args) const { return m_base->Call(std::forward<Args>(args)...); } std::shared_ptr<FunctionBase> m_base;};在std::for_each中看lambda和std::bind
Section titled “在std::for_each中看lambda和std::bind”std::for_each
Section titled “std::for_each”std::vector<int> vec= {1, 2, 3};std::for_each(vec.begin(), vec.end(), [](int& item) { item += 1;});std::for_each(vec.begin(), vec.end(), [](int item) { std::cout << item;});我们发现这里的lambda中的参数既可以int也可以int&,这是为什么呢,点进去看实现
可以看到就是普通的模板类,里面调用_Func(*_UFirst), _UFirst是迭代器类型,相当于指针,那么*_UFirst在这里就是int,相当于给_func(int)和_func(int&)传参,前者不会修改传入的参数,后者会修改传入的参数,自然两种都可以。
这里也可以看出lambda的一个优势,下面两种第一个编译不通过,第二个借助捕获可以编译通过
std::for_each(vec.begin(), vec.end(), [](int& item, int inc) { item += inc;});
int inc;std::for_each(vec.begin(), vec.end(), [inc](int& item) { item += inc;});lambda和std::bind
Section titled “lambda和std::bind”当我现在有两个函数,他们都是别人写的不能修改定义,比如
// 函数1void Increment(int &val, int inc) { val += inc;}
// 函数2std::for_each(_, _, OP);Op的参数个数小于Increment个数时,可以用lambda或bind来实现,lambda的存在让bind有点意义不大了
std::vector<int> data = {1, 2, 3};int inc = 5;
// std::bind auto my_inc = std::bind(Increment, inc, std::placeholders::_1); std::for_each(data.begin(), data.end(), my_inc);
// lambda std::for_each(data.begin(), data.end(), [inc](int& item) { Increment(item, inc); });
// Op参数大于Increment