现代C++学习笔记(7):函数对象
1
早在现代C++出现之前,即在2011年发布C++11标准之前,人们就早已不满经典的C++语言种种局限,而做了多种不同的尝试,比如发明了Java语言和C#语言。
Java语言从一开始,就是模仿C/C++的语法进行的设计,继承了面向对象的核心理念,同时舍弃了诸如指针等难以驾驭的“危险源”,试图构造一门全新的“胜过C++”的语言。
而C#语言则是作为.NET体系的第一个实现及其官方支持语言,诞生于微软帝国。要是探究相关历史,有两本经典老书很值得一提:《COM本质论》和《.NET本质论》。前者说“COM是一个更好的C++”,后者说“.NET是一个更好的COM”。
COM作为Windows的功能组件架构,在.NET普及之前曾经大行其道,它尝试对“过于简陋”的C++对象进行改良,在诸如接口定义、生存期管理、跨语言使用、跨进程甚至跨计算机调用等方面,做出了一系列改进。而.NET则更进一步,从运行时框架角度对上述特性做了重新设计和实现,甚至发明了C#这样一门新语言来支持它,使得它使用起来像C++,功能却远胜于C++。
然而,纵使如此,C++语言却从未被这些语言取代,各语言伴着自己的特性和生态圈,独立地发展着。有趣的是,随着二十多年过去,Java和C#,这两种语言,从设计理念,到实现细节,都彼此越来越像。而比较重要的,它们都是“纯”面向对象语言,在由它们编写的程序中,“一切皆对象”。
而C++语言却并非如此。C++是个多面手,是一个“多语言联邦”,面向对象只是它的其中一面。本次笔记,将介绍一种由语法技巧的使用,人工“创造”出来的对象:函数对象。
2
直接看一个例子:
1#include <iostream>
2
3class MakeDouble
4{
5public:
6 int operator()(int x) { return 2 * x; }
7};
8
9int main()
10{
11 MakeDouble f;
12 std::cout << f(42) << std::endl; // 84
13}可以看到,通过重载运算符 operator (),就能把对象实例模拟成一个函数。在上述例子中,变量“f”可以像函数一样被使用,带上小括号和参数,并返回计算结果。
3
由于这种函数对象是用 class 定义的,也就意味着我们可以在其内部加入成员变量,记录一些不同的参数。例如:
1#include <iostream>
2
3class Magnifier
4{
5public:
6 explicit Magnifier(int factor): factor_(factor) { }
7 int operator()(int x) { return x * factor_; }
8private:
9 int factor_;
10};
11
12int main()
13{
14 Magnifier make_double(2);
15 Magnifier make_triple(3);
16 std::cout << make_double(42) << std::endl; // 84
17 std::cout << make_triple(42) << std::endl; // 126
18 return 0;
19}同一个类定义,通过设置不同的初始化参数,形成功能各异的“函数对象”。
4
上次笔记中提到的 std::mem_fn,将成员函数包装成为一个对象,以避免直接使用危险的函数指针。其实现正是基于上述技巧原理。
由此,我们可以尝试自己实现个简版的 mem_fn:
1#include <iostream>
2#include <functional>
3
4class A
5{
6public:
7 void foo(int x)
8{
9 std::cout << "A::foo(" << x << ")" << std::endl;
10 }
11};
12
13class my_mem_fn
14{
15public:
16 explicit my_mem_fn(void (A::*func)(int)): func_(func) { }
17 void operator()(A& obj, int x) { (obj.*func_)(x); }
18private:
19 void (A::*func_)(int);
20};
21
22int main()
23{
24 A a;
25 a.foo(1); // A::foo(1)
26 auto f1 = std::mem_fn(&A::foo);
27 f1(a, 2); // A::foo(2)
28 auto f2 = my_mem_fn(&A:foo);
29 f2(a, 3); // A::foo(3)
30 return 0;
31}上述例子中,我们硬编码了函数类型,固定为一个int参数和无返回值。实际编程中,并不可能预见到函数的类型,于是就到模板上场了。读者可以自行继续尝试,这里暂不展开。
不过可以提示:在C++中,曾经引入过诸如 bind1st() 等,来帮助把一个函数封装成为对象,而在现代C++中,新标准及语法的引入(如可变参数模板),使得对各式各样无法预见的函数类型,能够进行有效的支持。
注:本文首发表于“不靠谱颜论”公众号,并同步至本站。