Effective STL~6 函数子、函数子类、函数及其他

函数子、函数子类的基本概念
所有重载了函数调用操作符(operator())的类都是一个函数子类(又称函数类型)。
从这些类创建的对象被称为函数对象,或函数子(functor)。
也就是说,函数子类是一种class,而函数子是这个class的一个object。函数子类可用作模板的参数类型,而函数子则不行。

与普通函数相比,函数子的一大优点是:函数子可能包含你所需要的状态信息,而普通函数无法保存状态。

第38条:遵循按值传递的原则来设计函数子类

函数指针按值传递
C/C++中,都不允许将一个函数作为参数传递给另一个函数,相反,你必须传递函数指针。C标准库qsort:
void qsort(void* base, size_t nmemb, size_t size, int(cmpfcn)(const void, const void*));
这里cmpfcn传递的实参是一个函数指针,其值从调用者一端拷贝到qsort函数中;也就是说,cmpfcn采用了值传递方式。
C/C++标准库函数都遵循了这一规则:函数指针按值传递。

如何强制按引用传递函数对象?
虽然STL使用者几乎不会这样做,但我们还是来看看如何按引用来传递函数对象。

template<class InputIterator,
       class Function>
       Function for_each(InputIterator first, InputIterator last, Function f);

class DoSomething // 函数子类/函数类型
{
public:
       void operator()(int x) { ... }
             ...
};

typedef deque<int>::iterator DequeIntIter;
deque<int> di;
...
DoSomething d; // 创建一个函数对象
...
// 类型参数DequeIntIter和DoSomething&来调用for_each, 强制d按引用传递并返回
for_each<DequeIntIter, DoSomething&>(di.begin(), di.end(), d);

函数子类旧的写法是继承自unary_function<int, void>,作为一元函数对象的基类,本身不重载operator(),由派生类完成。因此这种函数类型常被称为函数子类。

值传递函数对象与virtual函数
函数对象按值传递和返回,那么你必须确保编写的函数对象在警告传递后还能正常工作,这意味着:1)你的函数对象必须尽可能小,否则拷贝开销会非常高昂;2)函数对象必须是单态(非多态的),也就是说,它们不得使用virtual函数。

这是因为,如果参数类型是基类类型,而实参是派生类对象,那么在传递过程中会产生剥离问题(slicing problem,又译为切割问题):在对象拷贝过程中,派生部分可能会被去掉,而仅保留了基类部分。

效率很重要,避免剥离问题也很重要,但并不是所有函数子都很小巧,也不是所有的函数子都是单态的,而且函数子能携带一些你可能需要的状态新。因此,试图禁止多态的函数子是不切实际的。有没有一种两全其美的办法,既按值传递函数子,又能允许函数对象保留多态性?

答案是有的,办法是:将所需的数据和virtual函数从函数子类中分离出来,放到一个新的类中;然后,在函数子类中包含一个指针,指向这个新类的对象。
例如,如果你希望创建一个一个包含大量数据并且使用了多态性的函数子类:

// 原始的函数子类 不仅包含了多个数据, 而且包含virtual函数
// 如果值传递函数对象, 势必造成拷贝过程中的切割问题, 无法保留多态性
template<typename T>
class BPFC // BPFC = "Big Polymorphic Functor Class" (巨大的多态函数子类)
{
public:
       virtual void operator()(const T& val) const;
       ...
private:
       Widget w;
       int x;
       ...
};

修改:将所需的数据和virtual函数从函数子类分离出来,放到新类BPFCImpl中,然后,函数子类BPFC中包含一个指针指向新类BPFCImpl对象。

template<typename T> class BPFC; // 声明class BPFC

template<typename T>
class BPFCImpl
{
private:
       Widget w;
       int x;
       ...
       virtual ~BPFCImpl();
       virtual void operator()(const T& val) const;
       friend class BPFC<T>;
};

template<typename T>
class BPFC // 新的BPFC类, 短小、单态
{
public:
       void operator()(const T& val) const // 现在这是一个non-virtual函数, 将调用转到BPFCImpl中
       {
              pImpl->operator()(val); 
       }
       ...
private:
       BPFCImpl<T> *pImpl; // BPFC唯一数据成员
};

BPFC class的实现,只是简单地调用了BPFCImpl中相应的virtual函数,BPFC 本身变得小巧、单态,但可以访问大量状态信息而且行为上具备多态性。这种技术在设计模式中被称为“Bridge Pattern”(桥接模式),在Exceptional C++中被称为“Pimpl Idiom”(Pointer to implementation,指针指向实现)。
BPFC class的作者需要牢记,必须确保BPFC的copy构造函数能正确处理它所指的BPFCImpl对象。也许最简单而合理的做法是使用引用计数,如shared_ptr辅助类。

[======]

第39条:确保判别式是“纯函数”

  • 判别式(predicate):指一个返回值为bool类型(或者可以隐式转换为bool类型)的函数。STL中,标准关联容器的比较函数就是判别式;对于像find_if以及各种与排序有关的算法,判别式往往也被作为参数来传递。

  • 纯函数(pure function):指返回值仅仅依赖于其参数的函数。如假设f是一个纯函数,x、y是2个对象,那么只有当x和y值发生变化时,f(x, y)的返回值才可能发生变化。

C++中,纯函数所能访问的数据应该仅局限于参数以及常量(在函数声明周期内不会被改变,这样的常量应该被声明为const)。

  • 判别式类(predicate class)是一个函数子类(函数类型),它的operator()函数是一个判别式,也就是说返回true or false。STL中,凡是能接受判别式的地方,就能接受一个真正的判别式或判别式类的对象。

STL算法要求判别式函数必须是纯函数。

下面探讨一下为什么会有这样的要求。不妨先看看违法此约束的后果。

// 一个拙劣的判别式类设计
class Widget{...};

class BadPredicate
{
public:
       BadPredicate(): timesCalled(0) { } // timesCalled初始化为0
       bool operator()(const Widget&) // 判别式函数非纯函数
       {
              return ++timesCalled == 3;
       }
private:
       size_t timesCalled;
};

vector<Widget> vw;
...
// 删除第3个元素
vw.erase(remove_if(vw.begin(), vw.end(), BadPredicate()), vw.end()); // BadPredicate()构建临时对象

这段代码看似合理,但是在许多STL实现中,不仅删除vw容器中第3个元素,还同时删除了第6个。

remove_if通常实现方式

template<typename FwdIterator, typename Predicate>
FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p) // 判别式p是实参的拷贝
{
       begin = find_if(begin, end, p);
       if (begin == end) return begin;
       else {
              FwdIterator next = begin; // next已经指向待删除元素
              return remove_copy_if(++next, end, begin, p); // 从已经找到的第1个待删除元素, 继续往后拷贝并忽略函数对象p返回true的元素
       }
}

为了避免语言细节上栽跟头,最简单的解决途径就是在判别式类中,将operator()函数声明为const。

// 良好设计:使用const限定符限制函数子修改函数子类数据成员,但仍有bug
class BadPredicate
{
public:
       BadPredicate(): timesCalled(0) { } // timesCalled初始化为0
       bool operator()(const Widget&) const // 判别式函数
       {
              return ++timesCalled == 3; // 这里会报编译错误:const成员函数不能修改类的成员数据
       }
private:
       size_t timesCalled;
};

用const限定符限制operator()函数子修改函数子类的成员数据是必要的,但不足以完全解决问题,因为const函数仍然可以访问mutable数据成员(摆动场)、非const的局部static对象、非const的类static对象、命名空间中的非const对象,以及非const的全局对象。
一个正常的判别式的operator()肯定是const的,但应该还是一个“纯函数”。

// 糟糕的判别式例子:访问了非const局部static对象
bool antherBadPredicate(const Widget&, const Widget&) // 判别式不是纯函数
{
       static int timesCalled = 0; // 切记不可在判别式中访问非const局部static对象
       return ++timesCalled == 3;
}

[======]

第40条:若一个类是函数子,则应使它可配接

假设想在一个包含Widget对象指针的list容器中,找到第一个满足isInsteresting()条件的Widget指针,很容易做到:

class Widget { ... };
bool isInsteresting(const Widget* pw)
{
       return true;
}

list<Widget*> widgetPtrs;
...
// 在list中,找到第一个满足isInsteresting()(返回true)的元素
list<Widget*>::iterator i = find_if(widgetPtrs.begin(), widgetPtrs.end(),  isInsteresting);
if (i != widgetPtrs.end())
{
       ...
}

然而,如果想找到第一个不满足isInsteresting()条件的Widget指针,直接像这样使用not1却不容易。
STL 中,
not1用来构造一个与谓词结果相反的一元函数对象。
not2用来构造一个与谓词结果相反的二元函数对象。

// 编译无法通过
list<Widget*>::iterator i = find_if(widgetPtrs.begin(), widgetPtrs.end(),  not1(isInsteresting));

正确的做法是,在应用not1之前,必须先将ptr_fun应用在isInsteresting上:

// OK
list<Widget*>::iterator i = find_if(widgetPtrs.begin(), widgetPtrs.end(), not1(ptr_fun(isInsteresting)));

为什么应用not1之前,应用了ptr_fun就可以了呢?
因为ptr_fun为isInsteresting函数创建了一个函数对象,not1需要一个函数对象才能构造,而isInsteresting作为一个基本的函数指针,缺少not1所需要的类型定义。

可配接的函数对象
除了not1,还有not2、bind1st、bind2nd都要求一些特殊的类型定义,那些非标准的、与STL兼容的配接器通常也是如此。
提供了这些必要的类型定义的函数对象,称为可配接的(adaptable)函数对象,反之如果函数对象缺少这些类型定义,则称为不可配接的。
可配接的函数对象能与其他STL组件更为默契地协同工作,它们能应用于更多的上下文环境,因此你应当尽可能地使你编写的函数对象可配接。

“这些特殊的类型定义”具体是指:argument_type、first_argument_type、second_argument_type,以及result_type。不过,实际情况更加复杂,因为不同种类的函数子类所需要提供的类型定义也不尽相同,它们是这些名字的子集。除了要编写自定义的配接器,否则并不需要指定有关这些类型定义的细节,因为提供这些类型定义最简单的办法是让函数子从特定的基类继承(或者说,从一个基结构继承)。
例如,如果函数子类的operator()只有1个实参,那么它应该从std::unary_function继承;如果有2个实参,那么它应该从std::binary_function继承。

注意:unary_function和binary_function是STL模板,C++11以后已经弃用,不能直接继承,而应该继承他们实例化参数的结构。

// C++11以前,使用unary_function让MeetsThreshold可配接
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;
};

// C++11以后,直接重载operator()让MeetsThreshold可配接
template<typename T>
class MeetsThreshold // 去掉了继承自std::unary_function的声明
{
private:
       const T threshold;
public:
       MeetsThreshold(const T& threshold);
       bool operator()(const Widget&) const;
};

// C++11以前,使用binary_function让WidgetNameCompare可配接
struct WidgetNameCompare: public binary_function<Widget, Widget, bool>
{
       bool operator()(const Widget& lhs, const Widget& rhs) const;
};

// C++11以后,直接重载operator()让WidgetNameCompare可配接
struct WidgetNameCompare: public binary_function<Widget, Widget, bool>
{
       bool operator()(const Widget& lhs, const Widget& rhs) const;
};

注意:
1)一般情况下,传递给unary_function或binary_function的非指针类型需要去掉const和引用(&)部分。
2)如果operator()带有指针参数,则规则又有所不同,const和指针(*)不能去掉。

为什么要以unary_function和binary_function作为函数子的基类?
因为它们提供了函数对象配机器所需要的类型定义,通过简单的继承,就可以产生可配接的函数对象。

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));

注:要使用not1、bind2nd等,必须让函数继承自unary_function或binary_function,以达到可配接;C++17以后,可以用not_fn替换not1、not2;C++11以后,可使用bind替换bind1st、bind2nd。

[======]

第41条:理解ptr_fun、mem_fun和mem_fun_ref的来由

ptr_fun、mem_fun、mem_fun_ref究竟是什么?到底完成了什么工作?
这些函数一个主要任务是为了掩盖C++语言中的一个内在语法不一致问题。

现在我们有Widget类,一个测试Widget对象的函数test,用于存放Widget对象的容器

class Widget
{
       ...
};

void test(Widget& w); // 测试w,如果不能通过测试,就将它标记为“失败”

vector<Widget> vw;

为了测试w中每个Widget对象,可以用for_each算法,可调用物参数可以有如下方式:
1)直接使用test函数

// 调用#1:OK
for_each(vw.begin(), vw.end(), test);

2)假如test是Widget成员函数,即Widget支持自测。理想情况下,可以用for_each在vw中每个对象上都调用Widget::test成员函数

class Widget
{
public:
       ...
       void test(); // 自测,如果不通过,就把*this标记为“失败”
};

// 调用#2:不能通过编译
for_each(vw.begin(), vw.end(), &Widget::test); 

3)对于存放Widget*指针的容器,应该也可以通过for_each调用Widget::test

list<Widget*> lpw;
// 调用#3:不能通过编译
for_each(lpw.begin(), lpw.end(), &Widget::test);

为什么只有调用#1才能通过编译?
可以看下for_each算法实现:

template<typename InputIterator, typename Function>
Function for_each(InputIterator begin, InputIterator end, Function f)
{
       while (begin != end) f(*begin++);
}

可以看出for_each的实现是基于使用语法#1的事实。这是STL中一种很普遍的惯例:函数或函数对象在被调用的时候,总是使用non-member函数的语法形式。STL的算法(包括for_each)都硬性采用了语法#1,而只有调用#1与这座语法形式兼容。这说明了为什么调用#1能通过编译,而#2,#3不行。

mem_fun、mem_fun_ref存在的意义就是被用来调整(通过通过语法#2、#3被调用的)member函数,也能通过语法#1被调用。

mem_fun是函数模板,针对所配件的member函数原型不同(包括参数个数、常数属性的不同),有几种变化形式。其中一个声明:

// 该mem_fun声明针对不带参数的非const member函数;C是类,R是所指向的member函数的返回类型
template<typename R, typename C>
mem_fun_t<R, C>
mem_fun(R(C::*pmf)());

mem_fun带一个指向某个member函数的指针参数pmf,并且返回一个mem_fun_t类型的对象。mem_fun_t是一个函数子类,它拥有该成员函数的指针,并且提供operator()函数,在operator()中调用了通过参数传递进来的对象上的该member函数。

将Widget::test的地址传递给mem_fun函数后,函数内部构造一个mem_fun_t调用对象,内含一个函数指针(类型为Widget*)保存,可通过在mem_fun_t::opertor()中调用Widget::test()。这样语法#3被调整为语法#,就可以编译通过了。

list<Widget *> lpw; // 同前面代码
...
for_each(lpw.begin(), lpw.end(), mem_fun(&Widget::test)); // 现在可以通过编译

像mem_fun_t这样的类被称为函数对象配接器(function object adapter)
mem_fun_ref函数与此类似, 将语法#2调整为语法#1,并产生一个类型为mem_fun_ref_t的配接器对象。

ptr_fun,适用于普通函数生成函数对象配接器。
mem_fun,适用于成员函数生成函数对象配接器,调用成员函数的元素是指针类型。
mem_fun_ref,适用于成员函数生成函数对象配接器,调用成员函数的元素是对象类型。

[======]

第42条:确保less与operator<具有相同的语义

假设Widget包含一个重量值和一个最大速度值

class Widget
{
public:
       ...
       size_t weight() const
       {
              return weight_;
       }
       size_t maxSpeed() const
       {
              return maxSpeed_;
       }
private:
       size_t weight_;
       size_t maxSpeed_;
};

通常情况下,按重量对Widget对进行排序是最自然的方式。Widget的operator<反映了这点:

bool operator<(const Widget& lhs, const Widget& rhs)
{
       return lhs.weight() < rhs.weight();
}

但是在某种特殊情况下,我们需要创建一个按照最多速度进行排序的multiset容器。而multiset的默认比较函数是less,而less默认情况下会调用operator<来完成自己的工作。

方式1)考虑特化less,切断less和operator<之间的关系,从而只考虑Widget的最大速度。

// 特化less<Widget>
template<>
struct std::less<Widget>
{
       bool operator()(const Widget& lhs, const Widget& rhs) const
       {
              return lhs.maxSpeed() < rhs.maxSpeed();
       }
};

// 客户端
multiset<Widget> widgets;

注意:标准库允许特化std命名空间内的组件,但修改是被禁止的。

方式2)为multiset容器指定比较类型
在方式1中特化less的方法,也许能做到让mutiset按最大速度排序,但未必是合理的选择。因为operator<不仅仅是less的默认实现方式,也是程序员期望less所做的事情,让less不调用operator<而去做别的事情,这会无端地违背程序员的意愿。
我们可以不特化less以断开和operator<之间的联系,而只需要专门为容器添加一个比较类型

// 定义比较类型
struct MaxSpeedCompare
{
       bool operator()(const Widget& lhs, const Widget& rhs) const
       {
              return lhs.maxSpeed() < rhs.maxSpeed();
       }
};

// 客户端
multiset<Widget, WidgetNameCompare> widgets;

应当尽量避免修改less行为,因为这样做很可能会误导其他的程序员。如果使用less,不论显示地或隐式地,你都需要确保它与operator<具有相同的意义。
如果希望以一种特殊方式来排序对象,那么最好创建一个特殊的函数类型,名字不能是less。

[======]

posted @ 2021-12-25 00:23  明明1109  阅读(145)  评论(0编辑  收藏  举报