在 STL 中 functor 扮演着重要的角色。在 STL 中 functor 默默的为 algorithms 提供支持,因此 functor 通常与 algorithm 同时出现,更为准确的来说: “functor (包括functor adapter) 总是伴随着 algorithm 同时出现。”
一个常见的使用场景如下:(这段代码会打印出容器 v 中的所有元素)
1 | // 自己定义的一般函数 |
右侧除了说明 for_each 调用方式外,还包含了 for_each 算法的源代码。 for_each 函数会遍历当前的容器,通过 取值运算符(‘*’)不断的取出容器中的值,并作为参数传递给函数对象f, 函数对象指向的是我们定义的 myfun “functor”。这相当于调用 myfun中的 operator()方法。
另外,还有一个很重要的一点是 function object 总是被设计成 按值传递,这意味着通常是通过 copy 操作完成赋值的.
在接触 functor 时,另一个跳不过的部件也十分的重要,它们就是 funtion adaptors ,他们将 functor 改头换面,以提供给用户更容易使用的接口。通常,function adaptors 在封装 functor 时,会向 functor 提问(ask)。functor 必须作出回答,这样 function 才能完成封装任务。
1. STL 中的 functor 源码解析
一个典型的 STL 中的 functor 源代码如下:
1 | template <class T> |
negate 会将调用取负操作符(可能被重载过),并返回取负之后的值。注意返回的类型是 T,而不是 *T&*,你可能经常性的返回 reference 以获得更高的效率,然而在这里这样做很危险。
这一点在 《Effective C++ 》Item 21 中被提及。
此外,这里有几点需要注意:
- 使用了两个 const,这使得 该 functor 只与输入的参数有关。对于相同的参数,其返回的值应该是相同的,对于不同的值返回的类型。
- 这一点是上面的延伸 ———— functor 不应该维护状态。
- negate 继承了一个奇怪的东西?
下面我们慢慢回答 “如何书写出一个完美的 functor”。
2. const 的重要性以及通过传值
在 《effective C++》中作者曾经建议:”多使用 const 关键字”。
猜想一下,下面的这段代码的运行结果会是什么样子:
1 | class Predicate { |
我们尝试删除 vector 中第三个元素,然而上面的这段代码实际上会删除两个元素(一个是第三个元素,一个是第六个元素)!!
我们必须通过阅读 remove_if 的源代码才能理解程序的底层到底发生了什么。
1 | template <class ForwardIterator, class Predicate> |
当遇见第三个元素时,first 不等于 last,所以会调用 remove_copy_if 函数然而由于 pred 是通过传值传递的,所以这个时候中的 pred 中的 timeCalled 的值是 0!
你可能会认为是 STL 设计的不合理,所以才导致程序出现了 bug? 当我们尝试通过引用(reference)来传递 pred 对象时(这需要修改remove_if、find_if 、remove_copy_if三者), 此时程序能够顺利的完成给定的任务。
然而我们再来查看另一个例子:
1 | template <typename Iterator, typename Predication> |
上面的代码会在遇见容器v中的第三个元素时,打印出”访问第三个元素”。此时你可能会觉得当我们定义 algorithm 中的 Predication 对象,就应该使用按 reference 传递的方式。
然而,当我们简单的连续调用两次 myfun 之后,你会发现程序只会打印一次”访问第三个元素”。
1 | myfun(v.begin(), v.end(), p); |
对于相同的输入,我们的 function 每次的表现不一样?
这时如果你戏剧性的将 myfun中的函数对象f 改为按值传递,程序就可以顺利的打印两次”访问第三个元素”了;
怎么回事,有时按值传递 Predication 对象,程序会顺利的运行。有时我们按 reference 传递 Predication 对象 程序也能够顺利的运行。
要想做到行为统一,真正的秘诀在于使用 const, 以及按值传递 Predication。
1 | class Predicate { |
第二个 const 阻止了我们修改函数的状态,这样我们就可以避免了对于相同的输入函数会给出不同的输出结果。
而按值传递 Predication 除了这以外,还能让我们的代码保持与 STL 库的一致性。
3. 如何让我们自己编写的一般函数融入到 STL 库
当我们按照上面的”建议”编写出自己的函数之后,并不是一切都万事大吉了。
1 | //bool interesting(const int i ); |
你突然想的到上面的补集,也就是得到不感兴趣(not interesting)的数。而你对于 STL 函数十分的熟悉,你会想到使用 not1函数来得到 interesting 的相反数。
1 | find_if(ve.begin(), ve.end(), |
因为 not1 实际上应该是一种 functor adapter,它需要向 funtctor 提问才能顺利的运行。
- ptr_fun
要想让程序顺利的运行,一种方式是使用 ptr_fun 函数。1
find_if(ve.begin(), ve.end(), not1(ptr_fun(interesting)));
ptr_fun 本质上也是一种 function adapter。
- 继承 unary_function 或 binary_function
要想让你自己编写的函数真正的成为一个functor,除了使用 ptr_fun 之外,你还可以继承下面的两个仿函数。这两个一个分别对应 一元函数和二元函数。当你的 function class 继承他们之一时 struct 中的 typedef 也就被继承下来了。此时我们的 function 就可以回答 not1(functor adaptors 的提问了)。1
2
3
4
5
6
7
8
9
10
11
12
13//一元仿函数
template <class Arg, class Result>
struct unary_function {
typedef Arg argument_type; //参数类型
typedef Result result_type;//返回值类型
};
//二元仿函数
template <class Arg1, class Arg2, class Result>
struct binary_function {
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
};1
2
3
4
5
6
7
8template <typename T>
struct myfun: public std::unary_function<T, bool>{
public:
bool operator()(const int i ) {
return true; //都返回 true
}
};
find_if(v.begin(), v.end(), not1(myfun<int>()));
总结—成为一个 functor 的关键
- 一个优秀的 functor 的输出应该只与输入有关,让 functor 维持一个状态并不总是一件好事
- 让你的 function 融入到 STL 的 functor 体系中,会更便于使用。