[C++]高效定义STL比较函数的一些建议
来源:互联网 发布:mac中强制删除文件夹 编辑:程序博客网 时间:2024/05/16 04:59
函数与函数子
在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的某些配接器和算法在接受函数对象时,会考虑效率,从而使其为引用类型,如果此时你在模板参数中再声明为引用,引用的引用是不可编译的。
函数指针是按值传递意味着两件事:
- 你的函数对象必须足够小,否则复制的开销会非常大。
- 函数对象必须是单态的。因为多态不可行,会出现剥离问题。
但是视图禁止多态的函数子同样是不切实际的,解决方法就是把所需的数据和虚函数从函数子类中分离出来,放在一个新的类中,然后在函数子类中包含一个指针,指向这个新类的对象。
例如,你希望创建一个包含大量数据并且使用了多态性的函数子类:
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变量也是不行的。因为判别式一定要是纯函数!
所以,最重要的一点就是!确保判别式一定是纯函数!
- [C++]高效定义STL比较函数的一些建议
- [C++]STL算法高效使用的一些建议
- [C++]高效使用容器的一些建议
- [C++]高效使用迭代器的一些建议
- [C++]高效使用关联容器的一些建议
- [C++]高效使用c++11的一些建议
- url的一些正则处理 一个比较高效的函数
- 一些比较好的建议
- c语言一些比较特殊的函数
- STL 的一些数据结构比较
- 文本处理的一些C C++ STL函数
- 定义自己的STL map key的类型和比较函数
- C语言结构体定义函数指针的简单高效的使用例子
- c++ stl容器的一些比较
- 一些比较精妙的宏定义
- 一些比较好的宏定义
- 一些比较精妙的宏定义
- 让 C 程序更高效的 10 个建议
- IplImage,CvMat ,Mat关系与转换
- 从FineReport看开放式引擎API
- HDU 1021-Fibonacci Again
- python3实现简单爬虫功能
- 命令行运行java程序*.jar包
- [C++]高效定义STL比较函数的一些建议
- 设计模式之命令模式
- HyperPacer目标即场景
- 高并发文件下载服务器配置
- Java日历类Calendar
- DatePickerDialog和TimePickerDialog的简单使用
- Swift比Objective-C有什么优势
- DIV层的五条叠加法则
- Ubuntu 16.04网络配置