函数式设计模式
函数指针
首先需要定义函数指针类型:
using func_t = int (*)(int i);
// 等价于
typedef int (*func_t)(int i);
函数指针可以用于实现策略模式:
void test(func_t func) {
cout << func(2);
cout << func(3);
}
int twice(int i) {
return i * 2;
}
int main() {
test(twice); // 4 6
}
函数指针实现多态
int twice(int i) {
return i * 2;
}
int triple(int i) {
return i * 3;
}
int main() {
test(twice); // 4 6
test(triple); // 6 9
}
函数指针的缺陷
缺点:函数指针只能指向全局函数,无法保存状态。
int ntimes(int scale, int i) {
return i * scale;
}
int main() {
test(ntimes(2)); // 错误:没有这种语法
test(ntimes(3)); // 错误:没有这种语法
}
函数调用运算符
struct myclass {
void run(string name) {
cout << name << ",你好\n";
}
};
myclass mc;
mc.run("小切");
struct myclass {
void operator() (string name) {
cout << name << ",你好\n";
}
};
myclass mc;
mc. operator() ("小切");
struct myclass {
void operator() (string name) {
cout << name << ",你好\n";
}
};
myclass mc;
mc("小切");
重载了 operator()
后的 myclass 对象,调用起来就和普通函数一样。
仿函数
仿函数是一种重载了函数调用运算符的类,可以像函数一样使用:
struct twice_t {
int operator() (int i) {
return i * 2;
}
};
void test(twice_t func) {
cout << func(2);
cout << func(3);
}
twice_t twice;
int main() {
test(twice); // 4 6
}
仿函数的优势是可以保存状态:
struct ntimes_t {
int scale;
ntimes_t(int scale) : scale(scale) {}
int operator() (int i) {
return i * scale;
}
};
void test(ntimes_t func) {
cout << func(2);
cout << func(3);
}
int main() {
ntimes_t twice(2);
ntimes_t triple(3);
ntimes_t quadric(4);
test(twice); // 4 6
test(triple); // 6 9
test(quadric); // 8 12
}
仿函数的缺陷
缺点:需要在 test 中写明仿函数的具体类型,无法实现多态。
struct twice_t {
int operator() (int i) {
return i * 2;
}
};
struct triple_t {
int operator() (int i) {
return i * 2;
}
};
void test(twice_t func) {
cout << func(2);
cout << func(3);
}
twice_t twice;
triple_t triple;
int main() {
test(twice); // 4 6
test(triple); // 错误:test 只兼容了 twice_t 做参数
}
如何解决?
利用模板
template <class Func>
void test(Func func) {
cout << func(2);
cout << func(3);
}
twice_t twice;
triple_t triple;
int main() {
test(twice); // 4 6
test(triple); // 6 9
}
这样就实现了编译期多态,因为 test 函数的参数类型可以根据传入的具体函数类型进行推导。
模板传仿函数的缺陷
缺点:必须编译期确定,无法动态决定类型。
template <class Func>
void test(Func func) {
cout << func(2);
cout << func(3);
}
twice_t twice;
triple_t triple;
int main() {
bool ok;
cin >> ok;
test(ok ? twice : triple); // 错误:不兼容的类型之间不能三目
}
万能的 function 容器
std::function 采用了类型擦除技术,无需写明仿函数类的具体类型,能容纳任何仿函数或函数指针。
只需在模板参数中写明函数的参数和返回值类型即可,所有具有同样参数和返回值类型的仿函数或函数指针都可以传入。
struct twice_t {
int operator() (int i) {
return i * 2;
}
};
function<int (int)> twice = twice_t(); // 没问题,能接受仿函数
int triple(int i) {
return i * 3;
}
function<int (int)> function_triple = triple; // 没问题,能接受函数指针
struct ntimes_t {
int scale;
ntimes_t(int scale) : scale(scale) {}
int operator() (int i) {
return i * scale;
}
};
function<int (int)> quadric = ntimes_t(4); // 没问题,能接受带状态的仿函数
可以用 function 容器作为参数,就可以避免使用模板。
// 模板:性能优先
template <class Func>
void test(Func func) {
cout << func(2);
cout << func(3);
}
// 容器:灵活性优先
void test(function<int (int)> func) {
cout << func(2);
cout << func(3);
}
函数式为什么好?
有人说,function 底层依然是基于函数指针实现的,不是和虚函数一样低效吗?函数式相比传统面向对象好在哪里呢?
- 性能与灵活性的选择权
现实工程中,往往是 20% 的代码耗费了 80% 的计算机时间。我们只要优化这 20% 的瓶颈代码就可以。
虚函数实现的多态,是强制的,一旦用了虚函数,就没法把虚表去掉了,永远卡在你的对象类型里占 8 字节空间,永远只能以指针形态使用。
如果选择函数式编程范式,你可以在次要的业务逻辑代码中选择更灵活的 function 容器。
而在需要性能的瓶颈代码处,可以随时切换到基于模板的,更高性能的编译期多态。函数式给了你根据情况选择的自由度。
- function 有小对象优化
而且 function 内部具有类似于 string 小对象优化机制,对于较小的状态或没有状态的仿函数,就无需指针!无需堆内存分配!
而虚函数哪怕没有状态,由于虚表指针的存在,也总是需要用 new 创建,总是会造成大量的碎片化内存,因此即使同样选择了灵活性,function 依然比虚函数高效一点。
而且即使你的状态非常多,导致 function 不得不需要堆内存分配了,这一分配也不用你自己操心,不用手动 new,也不用手动 make_shared,function 内部自动帮你完成一切。
- lambda 表达式很方便
最重要的是,函数式编程范式可以便捷地利用 lambda 表达式就地创建仿函数对象,而面向对象需要大费周章定义一个类接口,然后再定义一个类实现虚函数,有时还需要分离声明和定义。
绕了个大圈子,不仅写起来痛苦,需要起名强迫症,而且看得人也头疼。lambda 表达式,就地创建,无需名字,更适合敏捷开发。