[C++]高效定义STL比较函数的一些建议

函数与函数子

在STL的使用中,我们经常需要自定义比较函数。本文将介绍如何完成这一类的函数,并且给出可靠而高效的使用建议。

1. mem_fun, ptr_fun, mem_fun_ref

mem_fun, ptr_fun, mem_fun_ref主要的任务是为了掩盖C++语言中一个内在的语法不一致的问题。

调用一个函数,C++提供了三种方法。

f(x); //  语法1:非成员函数的调用。
x.f(); // 语法2:成员函数的调用。
p->f(); // 语法3:指针调用成员函数。

对于语法1:

#include <iostream>
#include <vector>
using namespace std;
class Widget {
public:
};
void test(const Widget& one) {
    cout << "test fine!" << endl;
}
int main() {
    vector<Widget> vw;
    for_each(vw.begin(), vw.end(), ptr_fun(test));  //  这里是不是用ptr_fun都没有问题。
    return 0;
}

对于语法2:上面的写法就不再合适了,后文给出相应解释。正确的做法如下,调用mem_fun_ref。

#include <iostream>
#include <vector>
using namespace std;
class Widget {
public:
    void test() {
        cout << "test fine!" << endl;
    }
};
int main() {
    vector<Widget> vw;
    for_each(vw.begin(), vw.end(), mem_fun_ref(&Widget::test));
    return 0;
}

对于语法3:

#include <iostream>
#include <vector>
using namespace std;
class Widget {
public:
    void test() {
        cout << "test fine!" << endl;
    }
};
int main() {
    vector<Widget*> vw;
    for_each(vw.begin(), vw.end(), mem_fun(&Widget::test));
    return 0;
}

这三种不同情况的调用写法。那么原因究竟是什么呢?

从以下for_each的实现中,我们可以看出,for_each的实现是基于使用语法1的

template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
    for (; first != last; ++first) {
        f(*first);
    }
    return f;
}

这是STL中一个常见的惯例,函数或者函数对象在被调用时,总是使用非成员函数的语法形式。 所以直接使用语法2和语法3无法通过编译也就非常显然了。而mem_fun和mem_fun_ref就是为了把他们转换为相应的语法1的形式。

mem_fun的声明是这样的:

template <typename R, typename C>
mem_fun_t<R, C>
mem_fun(R(C::*pmf)());

mem_fun_t是一个函数对象配接器,他是一个函数子类,他拥有该函数对象的指针,并且提供了operator()函数,在 operator()中调用通过参数传递进来的对象上的该成员函数。

与此类似的,mem_fun_ref也做了相同的事情。

所以简单的说,每次在讲一个成员函数传递给一个STL组件的时候,就要使用它们。

2. 如果一个类是函数子,则应该使它可配接

在STL中4个标准的函数配接器(not1, not2, bind1st和bind2nd)都要求一些特殊的类型定义(分别是argument_type, first_argument_type, second_argument_type, return_type)。提供了这些类型定义的函数对象被称为可配接的函数对象。

#include <iostream>
#include <vector>
using namespace std;
bool check(int i) {
    return i == 3;
}
int main() {
    vector<int> v_i{1, 2, 3, 4, 5, 6l, 4};
    vector<int>::iterator iter = find_if(v_i.begin(), v_i.end(), not1(check));
    cout << *iter << endl;
    return 0;
}

这个代码的意图是找到第一个不满足check的元素。但发现这个函数是不能通过编译的。原因在与not1需要特殊的类型定义,所以需要做一个简单的调整才能通过编译。

vector<int>::iterator iter = find_if(v_i.begin(), v_i.end(), not1(ptr_fun(check)));

如果你需要编写函数子类的话,一定要从基结构中继承。operator()只有一个参数时,继承std::unary_function, 有两个参数时,继承std::binary_function。

#include <iostream>
#include <vector>
using namespace std;
class check : public unary_function<int, bool> {
private:
    int i;
public:
    check(int t = 0) {
        i = t;
    }
    bool operator()(const check& orig) const {
        return orig.i == i;
    }
};
int main() {
    vector<int> v_i{1, 2, 3, 4, 5, 6l, 4};
    vector<int>::iterator iter = find_if(v_i.begin(), v_i.end(), not1(check(1)));
    cout << *iter << endl;
    return 0;
}

如果此时没有继承于 unary_function

#include <iostream>
#include <vector>
using namespace std;
struct check : public binary_function<const int*, const int*, bool> {
    bool operator()(const int* i, const int* j) const {
        return *i < *j;
    }
};
int main() {
    vector<int*> v_i;
    v_i.push_back(new int (2));
    v_i.push_back(new int (4));
    v_i.push_back(new int (3));
    int* temp = new int (3);
    vector<int*>::iterator iter = find_if(v_i.begin(), v_i.end(), bind2nd(check(), temp));
    cout << **iter << endl;
    return 0;
}

3. 遵循按值传递的原则来设计函数子类

C++和C的标准库函数都遵循一个规则,函数指针都是按值传递的。

STL函数对象是函数指针的一种抽象和建模形式,所以,按照惯例,在STL中,函数对象在函数之间来回传递的时候也是按值传递的。这虽然可以通过强制类型来使其按引用传递,但这种做法是很危险的。因为STL的某些配接器和算法在接受函数对象时,会考虑效率,从而使其为引用类型,如果此时你在模板参数中再声明为引用,引用的引用是不可编译的。

函数指针是按值传递意味着两件事:

    1. 你的函数对象必须足够小,否则复制的开销会非常大。
    1. 函数对象必须是单态的。因为多态不可行,会出现剥离问题。

但是视图禁止多态的函数子同样是不切实际的,解决方法就是把所需的数据和虚函数从函数子类中分离出来,放在一个新的类中,然后在函数子类中包含一个指针,指向这个新类的对象。

例如,你希望创建一个包含大量数据并且使用了多态性的函数子类:

template <typename T>
class BPFC : unary_function<T, void> {
private:
    Widget w;
    int x;
public:
    virtual void operator()(const T& orig) const;
};

那么就应该创建一个小巧的,单态的类,其中包含一个指针,指向另一个实现的类,并且所有的数据和虚函数都放在哪个实现类中:

template <typename T>
class BPFC : unary_function<T, void> {
private:
    BPFCImpl<T> *pIMPl;
public:
    void operator()(const T& orig) const {
        pIMPl->operator()(orig);
    }
};
template <typename T>
class BPFCImpl : unary_function<T, void> {
private:
    Widget w;
    int x;
    virtual ~BPFCImpl();
    virtual void operator()(const T& orig) const;
    friend class BPFC<T>;
};

这里还有最后一个问题就是,要谨慎处理BPFC的拷贝构造函数,使其正确地处理它所指向的BPFCImpl对象。简单的方法是使用引用计数的智能指针。shard_ptr。

4. 确保判别式是“纯函数”

首先给出几个概念。

  • 判别式(predicate): 是一个返回值为bool的函数。对于STL中需要的比较函数一般都是用判别式的。
  • 纯函数(pure function):是指返回值仅仅依赖于其参数的函数。例如,同样的参数两次传入,其返回值是相同的。
  • 判别式类(predicate class):是一个函数子类,它的operator()是一个判别式,返回bool。STL中范式接受判别式的地方,就既可以接受一个真正的判别式,也可以接受一个判别式类的对象。

那么为什么要确保判别式是纯函数呢?

前文我们提到,函数对象是通过传值传递的,所以你应该设计出可以被正确复制的函数对象。除此以外,对于用作判别式的函数对象,当它们被复制时,还有另一个需要特别注意的地方:接受函数子的STL算法可能会先创建函数子的副本,然后存放起来再使用这些副本,而且有些STL算法实现也确实利用了这一特性,而这一特性的直接反应是:要求判别式函数一定是纯函数

加入我们设计了如下的判别式类,在调用remove_if就会出现问题。

class BadPredicate : public unary_function<Widget, bool> {
public:
    BadPredicate() : times(0) {}
    bool operator()(const Widget&) {
        return ++times == 3;
    }
private:
    int times;
};

我们来看看remove_if的函数实现就明白了。它的可能实现是这样的。

template<class ForwardIt, class UnaryPredicate>
ForwardIt remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p)
{
    first = std::find_if(first, last, p);
    if (first != last) {
            return first;
    } else {
            ForwardIt next = begin;
            return remove_copy_if(++next, last, first, p);
    }
}

本来我们是希望移除第3个元素,但通过这个实现的remove_if实际上我们还删除了第6个元素。因为predicate被按值传递,函数子类被重新构造。

实际上,解决方法最简单的就是,永远把operator()声明为const。但这还不够,因为即使是const成员函数,还是可以访问mutable, 非const的局部static对象,非const的类static对象,非const的全局对象等等。

上面的例子,就算是尝试使用static变量也是不行的。因为判别式一定要是纯函数!

所以,最重要的一点就是!确保判别式一定是纯函数!

时间: 2025-01-18 00:01:54

[C++]高效定义STL比较函数的一些建议的相关文章

C++ 之高效使用STL ( STL 算法分类)

http://blog.csdn.net/zhoukuo1981/article/details/3452118 C++ 之高效使用STL ( STL 算法分类),布布扣,bubuko.com

[C++]高效使用关联容器的一些建议

关联容器 本文介绍在关联容器中常见的一些的问题以及提升使用关联容器的建议. 1. 理解相等(equality)和等价(equivalence)的区别. 相等是以operator==为基础的.等价是以operator<为基础的. 例如find的定义是相等,他用operator==来判断,这是比较容易理解的. 而等价关系是以"在已排序的区间中对象值的相对顺序"为基础的.也就是说,如果两个值中任何一个(按照既定的排列顺序)都在另一个的前面,那么他们就是等价的. !(w1 < w2

C++ STL中哈希表 hash_map介绍

过map吧?map提供一个很常用的功能,那就是提供key-value的存储和查找功能.例如,我要记录一个人名和相应的存储,而且随时增加,要快速查找和修改: 岳不群-华山派掌门人,人称君子剑张三丰-武当掌门人,太极拳创始人东方不败-第一高手,葵花宝典... 这些信息如果保存下来并不复杂,但是找起来比较麻烦.例如我要找"张三丰"的信息,最傻的方法就是取得所有的记录,然后按照名字一个一个比较.如果要速度快,就需要把这些记录按照字母顺序排列,然后按照二分法查找.但是增加记录的时候同时需要保持记

关于STL中的map和hash_map

以下全部copy于:http://blog.chinaunix.net/uid-26548237-id-3800125.html 在网上看到有关STL中hash_map的文章,以及一些其他关于STL map和hash_map的资料,总结笔记如下:     1.STL的map底层是用红黑树实现的,查找时间复杂度是log(n):     2.STL的hash_map底层是用hash表存储的,查询时间复杂度是O(1):     3.什么时候用map,什么时候用hash_map?     这个药看具体的

c++笔记--stl的hash_map

以下内容是转载的:http://stlchina.huhoo.net/bin/view.pl/Main/STLDetailHashMap 详细解说STL hash_map系列 详细解说STL hash_map系列 0 为什么需要hash_map 1 数据结构:hash_map原理 2 hash_map 使用 2.1 一个简单实例 2.2 hash_map 的hash函数 2.3 hash_map 的比较函数 2.4 hash_map 函数 3 相关hash容器 4 其他 4.1 hash_map

STL hashmap介绍

0 为什么需要hash_map 用过map吧?map提供一个很常用的功能,那就是提供key-value的存储和查找功能.例如,我要记录一个人名和相应的存储,而且随时增加,要快速查找和修改: 岳不群-华山派掌门人,人称君子剑 张三丰-武当掌门人,太极拳创始人 东方不败-第一高手,葵花宝典 ... 这些信息如果保存下来并不复杂,但是找起来比较麻烦.例如我要找"张三丰"的信息,最傻的方法就是取得所有的记录,然后按照名字一个一个比较.如果要速度快,就需要把这些记录按照字母顺序排列,然后按照二分

STL 中的map 与 hash_map的理解

可以参考侯捷编著的<STL源码剖析> STL 中的map 与 hash_map的理解 1.STL的map底层是用红黑树存储的,查找时间复杂度是log(n)级别: 2.STL的hash_map底层是用hash表存储的,查询时间复杂度是常数级别: 3.什么时候用map,什么时候用hash_map? 这个要看具体的应用,不一定常数级别的hash_map一定比log(n)级别的map要好,hash_map的hash函数以及解决地址冲突等都要耗时,而且众所周知hash表是以空间效率来换时间效率的,因而h

读书笔记 effective c++ Item 26 尽量推迟变量的定义

1. 定义变量会引发构造和析构开销 每当你定义一种类型的变量时:当控制流到达变量的定义点时,你引入了调用构造函数的开销,当离开变量的作用域之后,你引入了调用析构函数的开销.对未使用到的变量同样会产生开销,因此对这种定义要尽可能的避免. 2. 普通函数中的变量定义推迟 2.1 变量有可能不会被使用到的例子 你可能会想你永远不会定义未使用的变量,你可能要再考虑考虑.看下面的函数,此函数返回password的加密版本,提供的password需要足够长.如果password太短,函数会抛出一个logic

STL之set详解(二)

首先来看看set集合容器: set集合容器实现了红黑树的平衡二叉树数据结构,在插入元素时它会自动调整二叉树的排列,把该元素放到适当的位置,并且 保证左右子树平衡.平衡二叉检索树采用中序遍历算法. 对于set,vector,map等等,它们的前向迭代器定义是这样的(以set为例): set<int>::iterator it; for(it=s.begin();it!=s.end();it++){} 那么反向迭代器呢? set<int>::reverse_iterator rit;