【Effective STL(6)】仿函数、仿函数类、函数等
2018-03-15 本文已影响14人
downdemo
38 把仿函数类设计为用于值传递
- STL函数对象在函数指针之后成型,因此STL习惯传给函数和从函数返回时,函数对象是值传递的,比如for_each算法通过值传递获取和返回函数对象
// for_each声明
template<class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f);
- 值传递的情况并不是完全打不破的,for_each可以显式指定参数类型,下面的代码使for_each通过引用传递和返回它的仿函数
class DoSomething : public unary_function<int, void> {
public:
void operator()(int x) {...}
...
};
typedef deque<int>::iterator DequeIntIter;
deque<int> di;
...
DoSomething d; // 建立一个函数对象
...
for_each<DequeIntIter, DoSomething&>(di.begin(), di.end(), d);
- 但STL的用户不能这样做,如果函数对象是引用传递,有些STL算法不能编译
- 函数对象以值传递和返回,因此需要确保两点,一是函数对象应该很小避免开销,二是函数对象不能用虚函数
- 但不是所有的仿函数都是小的、单态的。函数对象比函数优越的原因之一是仿函数可以包含需要的所有状态。为了实现多态,把虚函数放到另一个类中,给仿函数一个指向这个类的指针。比如希望实现下面这个包含多态的仿函数(这个实现是不对的,因为仿函数不能包含虚函数)
template<typename T> // Big Polymorphic Functor Class
class BPFC : public unary_function<T, void> {
private:
Widget w; // 这个类包含很多数据
Int x;
```
public:
virtual void operator()(const T& val) const;
...
};
- 正确的做法是建立一个包含一个指向实现类的指针的小而单态的类,然后把所有数据和虚函数放到实现类。这个做法在许多地方提及过,参考Effective C++ item34、Exceptional C++中的pimpl、设计模式中的Bridge模式
template<typename T>
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> {
private:
BPFCImpl<T> *pImpl; // BPFC唯一的数据
public:
void operator()(const T& val) const
{
pImpl->operator() (val);
}
...
};
39 用纯函数做判断式
- 返回一个bool的就叫判断式
- 纯函数是返回值只依赖于参数的函数,比如纯函数f(x, y)的返回值仅当x或y的值改变的时候才会改变
- 仿函数类的operator()函数是一个判断式
- 为了明白判断式函数必须是纯函数的原因,参考下面的判断式类,第三次调用时返回true,其他时候返回false,用这个类从一个vector<Widget>中除去第三个Widget
class BadPredicate : public unary_function<Widget, bool> {
public:
BadPredicate(): timesCalled(0) {}
bool operator()(const Widget&)
{
return ++timesCalled == 3;
}
private:
size_t timesCalled;
};
vector<Widget> vw;
...
vw.erase(remove_if(vw.begin(), vw.end(), BadPredicate()), vw.end());
- 看似合理,但它不仅会除去第三个元素,还会除去第六个,参考remove_if的实现
template <typename FwdIterator, typename Predicate>
FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p)
{
begin = find_if(begin, end, p);
if (begin == end) return begin;
else
{
FwdIterator next = begin;
return remove_copy_if(++next, end, begin, p);
}
}
- 判断式p先传给find_if,后传给remove_copy_if。find_if先接收了一个timesCalled为0的BadPredicate对象,find_if调用三次后对象返回true,然后才调用remove_copy_if,传p的另一个拷贝作为判断式,p的timesCalled成员仍为0。结果第三次remove_copy_if调用判断式也会返回true,因此remove_if最终会删除两个Widgets
- 避免碰到这种问题的方法是把判断式类的operator()函数声明为const,防止改变状态
class BadPredicate : public unary_function<Widget, bool> {
public:
bool operator()(const Widget&) const
{
return ++timesCalled == 3; // 错误,不能改变局部数据
}
};
- 把operator()声明为const是必要的,但还不够充分。行为良好的operator()当然是const,但它也得是一个纯函数,纯函数没有状态
bool anotherBadPredicate(const Widget&, const Widget&)
{
static int timesCalled = 0; // 错误,纯函数不能有状态
return ++timesCalled == 3;
}
40 使仿函数类可适配
- 在list中找到第一个符合条件的元素很简单
list<Widget*> widgetPtrs;
bool f(const Widget *pw);
list<Widget*>::iterator i = find_if(widgetPtrs.begin(), widgetPtrs.end(), f);
if (i != widgetPtrs.end()) { ... }
- 但如果要找第一个不符合条件的
list<Widget*>::iterator i =
find_if(widgetPtrs.begin(), widgetPtrs.end(), not1(f));
- 这样做会编译失败,因为not1不能直接对函数使用,所以要先用ptr_fun把函数转为函数对象
list<Widget*>::iterator i =
find_if(widgetPtrs.begin(), widgetPtrs.end(), not1(ptr_fun(f)));
- ptr_fun做的唯一的事是使一些typedef有效,四个函数适配器not1、not2、bind1st和bind2nd都需要这些typedef。这些typedef是argument_type、first_argument_type、second_argument_type和result_type
- std::unary_function和std::binary_function提供了这些typedef,用于给仿函数类继承。仿函数类的operator()带一个实参则继承std::unary_function,带两个实参则继承是std::binary_function
- unary_function和binary_function是模板,所以不能直接继承,必须从它们产生的类继承,而这需要指定实参类型。对于unary_function必须指定operator()的参数类型和返回类型,对于binary_function要指定operator()的两个参数类型和返回类型
template<typename T>
class MeetsThreshold : public std::unary_function<Widget, bool> {
private:
const T threshold;
public:
MeetsThreshold(const T& threshold);
bool operator() (const Widget&) const;
...
};
struct WidgetNameCompare : public std::binary_function<Widget, Widget, bool> {
bool operator()(const Widget& lhs, const Widget& rhs) const;
};
- 一般unary_function或binary_function的非指针类型都去掉了const和引用, 因此后一个operator()的实参类型传给binary_function的是Widget。现在可以直接对上面的仿函数类使用函数适配器
list<Widget> widgets;
...
list<Widget>::reverse_iterator i1 =
find_if(widgets.rbegin(), widgets.rend(), not1(MeetsThreshold<int>(10)));
Widget w(构造函数实参);
list<Widget>::iterator i2 =
find_if(widgets.begin(), widgets.end(), bind2nd(WidgetNameCompare(), w);
- 如果仿函数类没有继承unary_function或binary_function,上述代码就不能编译,因为not1和bind2nd都只和可适配的函数对象合作
41 了解使用ptr_fun、mem_fun和mem_fun_ref的原因
- 如果有一个函数f和一个对象x,要位于x的成员函数之外在x上调用f,有三种不同的语法实现这个调用
f(x); // 语法#1:f是一个非成员函数
x.f(); // 语法#2:f是一个成员函数且x是一个对象或对象的引用
p->f(); // 语法#3:当f是一个成员函数且p是一个对象的指针
- 假设有一个测试Widget的函数和一个Widget的容器,用for_each测试容器中的每个元素
void test(Widget& w);
vector<Widget> vw;
for_each(vw.begin(), vw.end(), test); // 调用#1(可以编译)
- 但如果test是Widget的成员函数,编译将失败
class Widget {
public:
void test(); // 失败则把*this标记为failed
};
for_each(vw.begin(), vw.end(), &Widget::test); // 调用#2(不能编译)
// 对指针的容器测试
list<Widget*> lpw;
for_each(lpw.begin(), lpw.end(), &Widget::test); // 调用#3(不能编译)
- 原因是for_each用的是语法#1,函数和函数对象使用非成员函数的语法形式调用是STL里的一个习惯
template<typename InputIterator, typename Function>
Function for_each(InputIterator begin, InputIterator end, Function f)
{
while (begin != end) f(*begin++);
}
- mem_fun和mem_fun_ref的作用就是让成员函数使用句法1调用
// 用于不带参数的non-const成员函数的mem_fun声明
template<typename R, typename C> // C是类,R是成员函数的返回类型
mem_fun_t<R,C> mem_fun(R(C::*pmf)());
- 返回的mem_fun_t类型的对象是一个仿函数类,容纳成员函数指针pmf,提供一个调用成员函数的operator()
for_each(lpw.begin(), lpw.end(), mem_fun(&Widget::test)); // 可以编译
- mem_fun适配语法#3到语法#1,mem_fun_ref函数适配语法#2到语法#1,只要传一个成员函数给STL组件就必须使用它们
- for_each没有使用ptr_fun增加的typedef,所以不必使用ptr_fun,当然加了也不会有变化。如果不清楚什么时候使用ptr_fun,就在每次传递函数给STL组件时都用上,除了降低可读性并不会有其他影响
for_each(vw.begin(), vw.end(), ptr_fun(test)); // 同调用#1
42 确定less<T>表示operator<
- 如果一个类A有x和y两个成员,用于A的operator<按x排序
class A {
public:
...
int x() const;
int y() const;
...
};
bool operator<(const A& lhs, const A& rhs)
{
return lhs.x() < rhs.x();
}
- 现在想建立一个按y排序的multiset<A>,而multiset的默认比较函数是less<A>,默认的less<A>通过调用A的operator<来工作,于是明显想到特化less<A>使其按y排序
template<>
struct std::less<A> : public std::binary_function<A, A, bool> {
bool operator() (const A& lhs, const A& rhs) const
{
return lhs.y() < rhs.y();
}
}
- 这种特化并不罕见,比如智能指针想让排序行为表现得像内建指针
namespace std {
template<typename T>
struct less<shared_ptr<T>>
: public binary function<boost::shared_ptr<T>, shared_ptr<T> bool> {
bool operator()(const boost::shared_ptr<T>& a, const boost::shared_ptr<T>& b) const
{
return less<T*>()(a.get(),b.get());
}
};
}
- 但operator<是实现less的默认方式,也是我们希望less做的,没有理由时不应该让less做其他事,回到最初的例子,如果希望按y排序,应当建立一个不叫less的仿函数类用于比较
struct yCompare : public binary_function<A, A, bool> {
bool operator()(const A& lhs, const A& rhs) const
{
return lhs.y() < rhs.y();
}
};
multiset<A, yCompare> a;