<学习笔记>Algorithm Library Design 算法库设计in c++ II(STL与泛型编程)
3.STL and Generic Programming
-
简介
STL的编程模式是泛型编程,有以下几个核心思想:
- 对于算法而言尽可能少的假设数据类型,反之也是如此,从而使得算法和数据能够尽可能好的彼此协作.
Expressing algorithms with minimal assumptions about data abstractions, and vice versa,
thus making them as interoperable as possible.
- 尽可能的将算法向上泛化而同时不丢失运行效率.即泛化的算法当特化之后其效率与原始的算法是一样的.
Lifting of a concrete algorithm to as general a level as possible without losing efficiency;
i.e., the most abstract form such that when specialized back to the concrete case
the result is just as efficient as the original algorithm.
- 当向上的泛化不能满足所有的算法需求,另外提出一个更泛化的版本,但是确保当有特定的需求时能够自动调用那个特化的效率最高的版本。
- Providing more than one generic algorithm for the same purpose and at the same level of abstraction,
when none dominates the others in efficiency for all inputs.
This introduces the necessity to provide sufficiently precise characterizations of the domain for which each algorithm is the most efficient.
-
概念和模型
考虑 swap函数template <class T> void swap( T& a, T& b) { T tmp = a; a = b; b = tmp; }当执行swap,实例化的时候,模板参数(place holder)T变成一个实际参数类型。但是编译成功的前提是该实参支持拷贝构造和赋值操作。注意编译器只做语法上的检查,具体你的拷贝构造函数在语义上是否正确由程序员自己确保。我们将一系列的需求集合称为一个概念concep,如果一个实参满足了一个概念的所有需求requirments,我们称它为给概念的一个模型model.在swap的例子中,int是可复制概念的一个模型。int is a model of the concept Assignable.Common basic concepts
与面向对象模型就行对比,概念conepts类比与虚基类(接口),模型model类比与子类dervied classes(实现).
-
基于iterator迭代器的泛型算法
算法抽象化的核心是泛型编程。一个方面就是将访问数据的接口用一个简单泛化的概念来实现。一个概念就是迭代器,它是指针的抽象。迭代器,可以指向数据,可以遍历容器中的数据。
//container对iterator的支持,[begin,end)区间访问,记住区间都是采用前闭后开的形式,即最有有一个passs the end 标志位,方便遍历. while(!= pass_the_end_pos)template <class T> class list { void push_back( const T& t); // append t to list. typedef ... iterator; iterator begin(); iterator end(); };//算法并不是为特定容器而写,采用iteratortemplate <class InputIterator, class T> bool contains( InputIterator first, InputIterator beyond, const T& value){ while ((first != beyond) && (*first != value)) ++first; return (first != beyond); }我们可以将上面的算法用于数组,iterator就是指针。
int a[100]; // ... initialize elements of a. bool found = contains( a, a+100, 42);可以用在数组的一部分进行bool in_first_half = contains( a, a+50, 42); bool in_third_quarter = contains( a+50, a+75, 42); 也可以用在list容器上list<int> ls; // ... insert some elements into ls. bool found = contains( ls.begin(), ls.end(), 42);//泛化的copy算法template <class InputIterator, class OutputIterator> OutputIterator copy( InputIterator first, InputIterator beyond, OutputIterator result){ while (first != beyond) *result++ = *first++; return result; }//从数组a1,拷贝到数组a2int a1[100]; int a2[100]; // ... initialize elements of a1. copy( a1, a1+100, a2);注意如果从a拷贝到list ls而ls当前是空的,则不能达到目的,因为ls.beign() == ls.end()STL解决这类问题的方法是提供概念之间的配接器。这里的配接器back_inserter是Output iteraoter概念的一个model。也就是说它支持数据的写入,它以list容器作为输入,当有数据写入iteraor的时候,其赋值函数=被调用,而=被实现为向其所拥有的容器尾部添加该元素。list<int> ls; copy( a1, a1+100, back_inserter(ls));后面会给出back_inserter的实现。同样的在STL里面也存在C++ IO/STREAM和iterator之间的适配器,
copy( istream_iterator<int>(cin), istream_iterator<int>(), ostream_iterator<int>( cout, "\n"));这里将把从标准输入得到的所有整型数字输出到标准输出,并且数字之间输出\n。istream_iterator<int>() 表示这个区间的尾部past-the-end position for this range
-
一个部分实现的iterator
template <class T> class Const_value { T t; public: // Default Constructible ! Const_value() {} explicit Const_value( const T& s) : t(s) {} // Assignable by default. // Equality Comparable (not so easy what that should mean here) bool operator==( const Const_value<T>& cv) const { return ( this == &cv); } bool operator!=( const Const_value<T>& cv) const { return !(*this == cv); } // Trivial Iterator: const T& operator* () const { return t; } const T* operator->() const { return & operator*(); } // Input Iterator Const_value<T>& operator++() { return *this; } Const_value<T> operator++(int) { Const_value<T> tmp = *this; ++*this; return tmp; } };
这个iterator会指向一个常量,并且访问的区间无限大。
int a[100]; Const_value<int> cv( 42); copy_n( cv, 100, a); // fills a with 100 times 42.
-
函数对象
所谓函数对象也就是仿函数,就是类内部重载实现了operator ()成员函数的类的对象实例。
与函数指针相比较,函数对象有很大的优越性,更适合做泛型函数的参数。
- 函数对象可以持有自己的局部状态。类对象,可以有自己的变量。(本小节后面给出例子)
- 函数对象在组件技术中有可适配性,可以将某些修饰条件加在其上改变其状态。(后面的小节给出例子)
函数对象的用法:
template <class T> struct equals { bool operator()( const T& a, const T& b) { return a == b; } };template <class InputIterator, class T, class Eq> bool contains( InputIterator first, InputIterator beyond, const T& value, Eq eq ) { while ((first != beyond) && ( ! eq( *first, value))) ++first; return (first != beyond); }int a[100]; // ... initialize elements of a. bool found = contains( a, a+100, 42, equals<int>());现在由于函数对象可以持有局部状态,我们可以实现一个带有误差范围的equals:
我觉得这个例子才有意义,不然没必要加equals函数作为一个contains函数的参数。
template <class T> struct eps_equals { T epsilon; eps_equals( const T& eps) : epsilon(eps) {} bool operator()( const T& a, const T& b) { return (a-b <= epsilon) && (b-a <= epsilon); } }; bool found = contains( a, a+100, 42, eps_equals<int>(1));这样显示查找成功,如果a中存在41,42或者43.
template <class T> struct count_equals { size_t& count; count_equals( size_t& c) : count(c) {} bool operator()( const T& a, const T& b) { ++count; return a == b; } }; size_t counter = 0; bool found = contains( a, a+100, 42, count_equals<int>(counter)); // counter contains number of comparisons needed.这个例子中conter就记录了查找到的次数。
-
Iterator traits
参见efc++ 条款47.struct iterator_over_ints { typedef int value_type; // ... };
template <class Iterator> struct iterator_traits { typedef typename Iterator::value_type value_type; // ... };
iterator_traits< iterator_over_ints >::value_type
//对指针的偏特化template <class T> struct iterator_traits<T*> { typedef T value_type; // ... };iterator_traits< int* >::value_typeiterator traits 同时也定义了difference_type,iterator_category,pointer类型,以及refernce类型。利用traits技术我们可以判定数据的类型从而采取适当的措施。template <class InputIterator, class T, class Eq >
bool contains( InputIterator first, InputIterator beyond, const T& value,
Eq eq = equals<typename iterator_traits<InputIterator>::value_type>()) {
while ((first != beyond) && ( ! eq( *first, value)))
++first;
return (first != beyond);
}// Alternative solution to default using overloaded dispatch function
//
//template <class InputIterator, class T>
//bool contains( InputIterator first, InputIterator beyond, const T& value) {
// typedef typename iterator_traits<InputIterator>::value_type value_type;
// typedef equals<value_type> Equal;
// return contains( first, beyond, value, Equal());
//}现在对于带有eq函数参数的contains也可以这么用了,其实就是因为a 是 int*,对于那个对指针偏特化的traits,得到a指向的数据类型int。其它的iterator类似。
assert( contains( a, a+6, 42));
assert( contains( a, a+3, 42));STL 在其它很多地方大量应用traits手法, char_traits to define the equality test and other operations for a character type.
In addition, this character traits class is used as a template parameter for the basic_string class template, which allows the
adaption of the string class to different character sets.
-
实现可配接的函数对象。
本节内容另可参考effective stl 第40条,若一个类是函数子,则应该使它可配接。
可配接特性是函数对象独有的,函数指针则不能。A function pointer can be a valid model for a function object, but it cannot be a valid model of an adaptable function object.
在需要函数对象的地方函数指针也可以胜任,但是如果需要一个可配接函数对象,那么函数指针无能为力。
前面的函数对象equals from above could be derived from std::binary_function to declare the appropriate types.
#include <functional> template <class T> struct equals : public std::binary_function<T,T,bool> { bool operator()( const T& a, const T& b) { return a == b; }
template <class Arg1, class Arg2, class Result> struct binary_function { typedef Arg1 first_argument_type; typedef Arg2 second_argument_type; typedef Result result_type; };
从binary_function在STL中的定义,我们可以看成equals继承binary_function,其实就是告诉它及以后的应用程序我的两个输入的参数类型是什么,输出参数的类型是什么。
先看一个应用实例,not1的运用
Example
// not1 example #include <iostream> #include <functional> #include <algorithm> using namespace std; struct IsOdd : unary_function<int,bool> { bool operator() (const int& x) const {return x%2==1;} }; int main () { int values[] = {1,2,3,4,5}; int cx; cx = count_if ( values, values+5, not1(IsOdd()) ); cout << "There are " << cx << " elements with even values.\n"; return 0; }Output:
There are 2 elements with even values.
计算values[]中偶数的数目,但是我们只有IsOdd函数,在不想再写IsEven的情况下,利用not1配接器我们可以方便的直接用
not1(IsOdd)代替IsEven函数。但是要注意我们定义的IsOdd需要继承unary_function<int,bool>来具有可配接的特性,否则
无法使用not1。这里unary_function指有一个输入参数,一个输出参数的函数对象的配接起。为什么需要继承它呢。
看一下not1的实现:
template <class Predicate> inline unary_negate< Predicate> not1( const Predicate& pred) { return unary_negate< Predicate>( pred); }
可以看到not1只提供一个接口,算是unary_negate的一个helper函数,它内部调用unary_negate< Predicate>( pred)实现。
template <class Predicate> class unary_negate : public unary_function< typename Predicate::argument_type, bool> { protected: Predicate pred; public: explicit unary_negate( const Predicate& x) : pred(x) {} bool operator()(const typename Predicate::argument_type& x) const { return ! pred(x); } };这里要调用!prd(x)我们需要知道x的类型,operator()(const typename Predicate::argument_type& x),这个类型由Predicate提供,具体在这里也就是由实参IsOdd提供。not1(IsOdd())则Predicate被实参化为类型IsOdd.所以我们需要IsOdd类提供它的operator()操纵的数据的类型。这也就是为什么我们需要继承unary_function<int,bool>。内部实现上的繁琐换来用户应用的方便,极大的灵活性。如果操作的函数对象是有两个输入参数的则要用not2,同时函数对象要继承binary_function.具体应用程序用iterator遍历区间,假定iterator iter,对每个遍历到的数据,调用not1(IsOdd())(*iter)->unary_negate(IsOdd())(*iter)-> !IsOdd()(*iter) //IsOdd()是一个临时的函数对象再看bind2nd的应用实例int main( int argc, char** argv) { if ( argc != 2) throw( "usage: remove_if_divides integer\n"); remove_copy_if( istream_iterator<int>(cin), istream_iterator<int>(), ostream_iterator<int>(cout, "\n"), not1( bind2nd( modulus<int>(), atoi( argv[1])))); return 0; }这个例子是将从标准输入输入的所有整数选择符合条件的拷贝到输出。这里的条件是不能被程序输入参数atoi( argv[1]))所整除。
bind2nd也是一个hepler函数,实际产生一个binder2nd函数对象。它接受一个bianry function对象,一个固定值,返回一个unary function对象,两个输入参数变为一个输入参数,原来的
第二个参数是被绑定一个固定值。
关于bind2nd
Return function object with second parameter binded
This function constructs an unary function object from the binary function object op by binding its second parameter to the fixed value x.
template < class Operation, class Tp> inline binder2nd< Operation> bind2nd( const Operation& fn, const Tp& x) { typedef typename Operation::second_argument_type Arg2_type; return binder2nd< Operation>( fn, Arg2_type(x)); }template <class Operation> class binder2nd : public unary_function< typename Operation::first_argument_type, typename Operation::result_type> { protected: Operation op; typename Operation::second_argument_type value; public: binder2nd( const Operation& x, const typename Operation::second_argument_type& y) : op(x), value(y) {} typename Operation::result_type operator()(const typename Operation::first_argument_type& x) const { return op(x, value); } };
因此 not1(bind2nd(modulus<int>(), num))(*iter) –> !bind2nd(modulus<int>(), num)(*iter)
–>!binder2nd(modulus<int>(), num)(*iter)->!modulus<int>(*iter, num)
-
配接器back_inserter的实现
template <class InputIterator, class OutputIterator>
OutputIterator copy( InputIterator first, InputIterator beyond, OutputIterator result){
while (first != beyond) *result++ = *first++;
return result;
}
list<int> ls; copy( a1, a1+100, back_inserter(ls));//back_inserter又是一个helper函数,内部调用back_insert_iteratortemplate <class Container> inline back_insert_iterator<Container> back_inserter(Container& x) { return back_insert_iterator<Container>(x); }
template <class Container> class back_insert_iterator { protected: Container* container; public: typedef Container container_type; typedef output_iterator_tag iterator_category; typedef void value_type; typedef void difference_type; typedef void pointer; typedef void reference; explicit back_insert_iterator(Container& x) : container(&x) {} back_insert_iterator<Container>& operator=(const typename Container::value_type& value) { container->push_back(value); //key magic is here! return *this; } back_insert_iterator<Container>& operator*() { return *this; } back_insert_iterator<Container>& operator++() { return *this; } back_insert_iterator<Container>& operator++(int) { return *this; } };
-
编译时根据迭代器类型进行函数分配(重载)
struct input_iterator_tag {}; struct output_iterator_tag {}; struct forward_iterator_tag : public input_iterator_tag {}; struct bidirectional_iterator_tag : public forward_iterator_tag {}; struct random_access_iterator_tag : public bidirectional_iterator_tag {};An iterator is assumed to have a local type iterator_category that is defined to be one of these tags.struct Some_iterator { typedef forward_iterator_tag iterator_category; // ... };This iterator category is accessed using iterator traits. Now we can implement a generic distance function (original implementation as it is in the STL):template <class InputIterator> inline typename iterator_traits<InputIterator>::difference_type __distance( InputIterator first, InputIterator last, input_iterator_tag) { typename iterator_traits<InputIterator>::difference_type n = 0; while (first != last) ++first; ++n; return n; } template <class RandomAccessIterator> inline typename iterator_traits<RandomAccessIterator>::difference_type __distance( RandomAccessIterator first, RandomAccessIterator last, random_access_iterator_tag) { return last - first; } template <class InputIterator> inline typename iterator_traits<InputIterator>::difference_type distance( InputIterator first, InputIterator last) { typedef typename iterator_traits<InputIterator>::iterator_category Category; return __distance(first, last, Category()); }