插图
(题图来自网络并做适当修改)

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

直接看一个例子:

cpp
 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 定义的,也就意味着我们可以在其内部加入成员变量,记录一些不同的参数。例如:

cpp
 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:

cpp
 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++中,新标准及语法的引入(如可变参数模板),使得对各式各样无法预见的函数类型,能够进行有效的支持。

--- END ---

注:本文首发表于“不靠谱颜论”公众号,并同步至本站。