Effective STL - 05 - Functor

Functor

条款38:把仿函数类设计为用于值传递

C 中的 qsort 使用的 函数指针 就是指针的拷贝,因此是一个默认的值传递。

因为函数对象以值传递和返回,你的任务就是确保当那么传递(也就是拷贝)时你的函数对象行为良好。这暗示了两个东西。第一,你的函数对象应该很小。否则它们的拷贝会很昂贵。第二,你的函数对象必须单态(也就是,非多态)——它们不能用虚函数。

不是所有的仿函数都是小的、单态的。函数对象比真的函数优越的的原因之一是仿函数可以包含你需要的所有状态。有些函数对象自然会很重,保持传这样的仿函数给STL算法和传它们的函数版本一样容易是很重要的。(像之前提到的算法中 Algorithms ,Points 类就使用了 PointsAverage 来保存对应的状态,我的实现使用了 lambda 和引用来保存求和的结果)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
使用 Bridge 模式
*/

template<typename T> // 用于修改的BPFC
class BPFCImpl:
public unary_function<T, void> { // 的新实现类
private:
Widget w; // 以前在BPFC里的所有数据
int x; // 现在在这里
...
virtual ~BPFCImpl(); // 多态类需要
// 虚析构函数
virtual void operator()(const T& val) const;
friend class BPFC<T>; // 让BPFC可以访问这些数据
};

template<typename T>
class BPFC:
public unary_function<T, void> { // 小的,单态版的BPFC
private:
BPFCImpl<T> *pImpl; // 这是BPFC唯一的数据
public:
void operator()(const T& val) const // 现在非虚;
{ // 调用BPFCImpl的
pImpl->operator() (val);
}
...
};

因为我勾勒出的基本技术在C++圈子中已经广为人知了。《Effective C++》的条款34中有。在Gamma等的《设计模式》[6]中,这叫做“Bridge模式”。Sutter在他的《Exceptional C++》[8]中叫它“Pimpl惯用法”。

从STL的视角看来,要记住的最重要的东西是使用这种技术的仿函数类必须支持合理方式的拷贝。如果你是上面BPFC的作者,你就必须保证它的拷贝构造函数对指向的BPFCImpl对象做了合理的事情。

条款39:用纯函数做判断式

判断式是返回bool(或者其他可以隐式转化为bool的东西)。判断式在STL中广泛使用。标准关联容器的比较函数是判断式,判断式函数常常作为参数传递给算法,比如find_if和多种排序算法。(排序算法的概览可以在条款31找到。):

  • 纯函数是返回值只依赖于参数的函数。如果f是一个纯函数,x和y是对象,f(x, y)的返回值仅当x或y的值改变的时候才会改变。 在C++中,由纯函数引用的所有数据不是作为参数传进的就是在函数生存期内是常量。(一般,这样的常量应该声明为const。)如果一个纯函数引用的数据在不同次调用中可能改变,在不同的时候用同样的参数调用这个函数可能导致不同的结果,那就与纯函数的定义相反。
  • 一个判断式类是一个仿函数类,它的operator()函数是一个判断式,也就是,它的operator()返回true或false(或其他可以隐式转换到true或false的东西)。正如你可以预料到的,任何STL想要一个判断式的地方,它都会接受一个真的判断式或一个判断式类对象。

在很多算法里的要求如下:

1
find_if(InputIt begin, InputIt end, Pred pred);

这里的 Pred 就是一个判断式(谓语,主谓宾,谓语只起到连接主语和宾语的用途),全称叫做 Predicate。

对于判断式来说,不要包含任何的状态:

  • 对于函数,就是不要含有可变的 static 变量,且影响到函数的返回结果
  • 对于函数对象,不要改变类成员
  • 对于以上二者,不要调用任何的全局 static 变量

条款40:使仿函数类可适配

一个普通的仿函数类只需要实现自己的 operator() 方法即可,但是这样可以无法应用于一些适配器,例如 not1bind2nd 等。

为了使得自己实现的仿函数类可适配,需要实现一些 typedef,应该是提供给调用者的 traits。一般的做法是继承 unary_function 或者 binary_function提供这些必要的typedef的函数对象称为可适配的,而缺乏那些typedef的函数对象不可适配。可适配的比不可适配的函数对象可以用于更多的场景,所以只要能做到你就应该使你的函数对象可适配。

operator()带一个实参的仿函数类,要继承的结构是std::unary_function。operator()带有两个实参的仿函数类,要继承的结构是std::binary_function。

bind 替代的是 bind1st、bind2nd,std::function 替代的是上面提到的 std::unary_function 以及 std::binary_function。

本条款的主题就是,如果你写了一个仿函数类,最好根据仿函数类的操作,继承上 unary_function 和 binary_function 使得仿函数类可适配。

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
必须继承 binary_function, 否则没有 first_argument_type second_argument_type 等 类型
也可以使用 C++ 11 推出的 std::function 替代上面的二者
*/
struct MyCmp : binary_function<int, int, bool> {
// struct MyCmp : std::function<bool (int, int)> {
// struct MyCmp {
// 一个 Predicate 不应该有自己的状态
// 因此不应该修改类内的元素
bool operator()(int a, int b) const {
return a < b;
}
};

/*
编写一个自己实现的 可适配的仿函数类, 而不是一个 binder
*/
template <typename FirstArg, typename SecondArg, typename RetArg>
struct MyCmp2 {
using first_argument_type = FirstArg;
using second_argument_type = SecondArg;
using result_type = RetArg;

RetArg operator()(FirstArg first, SecondArg second) const {
return first < second;
}
};

vector<int> v = {0, 1, 2, 3, 4, 5};
vector<int> v2; // 0 1 2
copy_if(v.begin(), v.end(), back_inserter(v2), bind2nd(MyCmp(), 3));

vector<int> v3; // 3 4 5
copy_if(v.begin(), v.end(), back_inserter(v3), not1(bind2nd(MyCmp2<int, int, bool>(), 3)));

条款41:了解使用ptr_fun、mem_fun和mem_fun_ref的原因

STL 中有三种函数调用方法:

1
2
3
test(p);        // STL 默认接受
p.test(); // 成员函数, 需要 mem_fun 适配器
(&p)->test(); // 成员函数, 指针运算符调用, 需要 mem_fun_ref 适配器

STL 默认只接受第一种写法。

一个与ptr_fun有关的可选策略是只有当你被迫时才使用它。如果当typedef是必要时你忽略了它,你的编译器将退回你的代码。然后你得返回去添加它。mem_fun和mem_fun_ref的情况则完全不同。只要你传一个成员函数给STL组件,你就必须使用它们,因为,除了增加typedef(可能是或可能不是必须的)之外,它们把调用语法从一个通常用于成员函数的适配到在STL中到处使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename T>
struct Point {
void print() const {
cout << "(" << x << ", " << y << ")" << endl;
}

T x, y;
};

/**
注意: 只有 Point<float>* 的容器才能调用 mem_fun(&Point<float>::print)
因为 mem_fun 的实现里面调用了指针运算符, 所以对象不能调用
对应的, mem_fun_ref 调用了 成员运算符, 所以指针不能调用
*/

int main() {
vector<Point<float>> vp = {{0.3f, 0.9f}, {1.6f, 2.4f}, {4.1f, 5.2f}};
vector<Point<float>*> vpPtr = {&vp[0], &vp[1], &vp[2]};
for_each(vpPtr.begin(), vpPtr.end(), mem_fun(&Point<float>::print));
for_each(vp.begin(), vp.end(), mem_fun_ref(&Point<float>::print));
}

mem_fun 和 mem_fun_ref 的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template <class _Result, class _Ty>
class const_mem_fun_t : public unary_function<const _Ty*, _Result> { // functor adapter (*p->*pfunc)(), const *pfunc
public:
explicit const_mem_fun_t(_Result (_Ty::*_Pm)() const) : _Pmemfun(_Pm) {}

_Result operator()(const _Ty* _Pleft) const {
return (_Pleft->*_Pmemfun)();
}

private:
_Result (_Ty::*_Pmemfun)() const; // the member function pointer
};

template <class _Result, class _Ty>
_NODISCARD const_mem_fun_t<_Result, _Ty> mem_fun(_Result (_Ty::*_Pm)() const) {
return const_mem_fun_t<_Result, _Ty>(_Pm);
}

template <class _Result, class _Ty>
class const_mem_fun_ref_t : public unary_function<_Ty, _Result> { // functor adapter (*left.*pfunc)(), const *pfunc
public:
explicit const_mem_fun_ref_t(_Result (_Ty::*_Pm)() const) : _Pmemfun(_Pm) {}

_Result operator()(const _Ty& _Left) const {
return (_Left.*_Pmemfun)();
}

private:
_Result (_Ty::*_Pmemfun)() const; // the member function pointer
};

template <class _Result, class _Ty>
_NODISCARD const_mem_fun_ref_t<_Result, _Ty> mem_fun_ref(_Result (_Ty::*_Pm)() const) {
return const_mem_fun_ref_t<_Result, _Ty>(_Pm);
}

条款42:确定less表示operator<

operator<不仅是实现less的默认方式,它还是程序员希望less做的。让less做除operator<以外的事情是对程序员预期的无故破坏。它与所被称为“最小惊讶的原则”相反。它是冷淡的。它是低劣的。它是坏的。你不该那么做。

如果你使用less(明确或者隐含),保证它表示operator<。如果你想要使用一些其他标准排序对象,建立一个特殊的不叫做less的仿函数类


Effective STL - 05 - Functor
http://hebangwen.github.io/2024/03/17/Functor/
作者
何榜文
发布于
2024年3月17日
许可协议