代码改变世界

斩获新知——记一次reverse的实现过程

2014-11-10 12:06  Peter87  阅读(2863)  评论(4编辑  收藏  举报
最近学习C++,在实现reverse模板函数的时候,从一个小问题开始,在对这个问题的旁敲侧击当中带起了更多疑惑,顺藤摸瓜之后,尽管没有将诸多问题完美解答,但整个过程下来却也似有所获。最初的问题从使用C++实现reverse模板函数时碰到的swap问题开始,随之在翻查STL中reverse源码的实现过程当中产生了其他疑问,如__ITERATOR_CATEGORY,__VALUE_TYPE,__STL_REQUIRES宏的作用,do...while(0)技巧。之后在查找一些资料之后总算对这些问题都有一个解答,如下予以记录。 

让swap函数支持迭代器

reverse函数的功能很简单。在C语言的编程场景中通常用在对字符串的逆转。在C++的STL中,将其实现为算法当中的一员,其目的自然是为了支持对多种容器的操作。下面是自己第一次尝试对其进行的实现,但在开头就遇到问题。 
 1 // version 0.1版本,问题版本
 2 template <class IN>
 3 void swap_my(IN left, IN right)
 4 {
 5    // 如何完成?
 6 }
 7  
 8 template <class IN>
 9 void reverse_my(IN begin, IN end)
10 {
11     while ((begin != end) && (begin != --end))
12     {
13         swap_my(begin, end);
14         ++begin;
15     }
16 }
17

这是一个没有完工的版本。在编写swap_my的时候写不下去了,这个时候尽管可以通过IN知道迭代器的类型,也能够通过*IN实现对迭代器所指向元素的访问,但这个时候我需要知道迭代器所指向的元素的类型(这里将迭代器所指向的元素的类型称为其value type,下文其他地方沿用此称谓),以便于申请一局部变量来协助完成两个变量值的对换,这便是第一个问题。思前想后没有想出答案,随即更换了一种思路,使用引用来完成其功能,但这随之而来也需要修改revers现中对于swap_my的调用方式,如下: 

 1 // version 1.0版本,swap仅支持引用参数
 2 
 3 template <class IN>
 4 
 5 void swap_my(IN& left, IN& right)
 6 {
 7     IN tmp = left;
 8     left = right;
 9     right = tmp;
10 }
11  
12 template <class IN>
13 void reverse_my(IN begin, IN end)
14 {
15     while ((begin != end) && (begin != --end))
16     {
17         swap_my(*begin, *end);
18         ++begin;
19     }
20 }

 version1.0版本通过"引用"的方式实现了swap_my函数,这的确是一个可以正常运作的reverse版本,但并不完美(在后文可见即便reverse实现上使用了支持迭代器的swap_my版本也仍旧有其不足)。回到最初的问题上面,上面的swap_my版本仅仅引用参数,如果想让swap_my也支持迭代器参数,是不是没有其他办法了?毕竟这要求也不高,STL当中的库函数不都要求要支持迭代器参数嘛。

 

苦思冥想仍旧不得其解。于是开始查找资料,之后在翻看《STL源码剖析》的过程当中找到了一种方法,也就是借助编译器的参数推导功能,来完成swap对于迭代器的支持。

 1 // version 1.1版本,swap仅支持迭代器参数
 2 template <class IN, class T>
 3 void swap_my(IN begin, IN end, T t)
 4 {
 5     T tmp = *begin;
 6     *begin = *end;
 7     *end = tmp;
 8 }
 9  
10 template <class IN>
11 void swap_iter(IN begin, IN end)
12 {
13     swap_my(begin, end, *begin);
14 }
15  
16 template <class IN>
17 void reverse_my(IN begin, IN end)
18 {
19     while ((begin != end) && (begin != --end))
20     {
21         swap_iter(begin, end);
22         ++begin;
23     }
24 }

将此版本与version1.0版本相比较,其实也就是在reverse与swap_my之间多了一次转换,这次转换通过对迭代器的“解引用(dereference)”操作,将迭代器指向元素的类型活脱脱的剥离了出来,大妙哉!

省思录作为一名程序员,心中虽然明白一些被业界精灵共同推崇的一些编程书籍,应该早日研读,并借此进阶技术并修得毕业。但是实际当中并没有做好。这一点是自己需要立即加以改进的地方。 

不完整的reverse

原本以为version1.1版本已经是一个很OK的实现版本。但在对比参照了STL当中reverse模板的实现之后发现完全不如自己所想的那样,非但如此,源码当中的实现似乎还藏有不少高深的技法,自己反倒被弄得愈加糊涂。下面一起看下STL当中是如何制造经典的reverse:)
 1 template <class _ForwardIter1, class _ForwardIter2, class _Tp>
 2 inline void __iter_swap(_ForwardIter1 __a, _ForwardIter2 __b, _Tp*) {
 3   _Tp __tmp = *__a;
 4   *__a = *__b;
 5   *__b = __tmp;
 6 }
 7  
 8 template <class _ForwardIter1, class _ForwardIter2>
 9 inline void iter_swap(_ForwardIter1 __a, _ForwardIter2 __b) {
10   __STL_REQUIRES(_ForwardIter1, _Mutable_ForwardIterator);
11   __STL_REQUIRES(_ForwardIter2, _Mutable_ForwardIterator);
12   __STL_CONVERTIBLE(typename iterator_traits<_ForwardIter1>::value_type,
13                     typename iterator_traits<_ForwardIter2>::value_type);
14  
15  __STL_CONVERTIBLE(typename iterator_traits<_ForwardIter2>::value_type,
16                     typename iterator_traits<_ForwardIter1>::value_type);
17   __iter_swap(__a, __b,
18  __VALUE_TYPE(__a));
19 }
20 // 以上为swap支持迭代器参数的实现,STL中的reverse实现并未使用支持引用参数的swap。支持引用参数的swap作为单独的库函数出现。
21  
 1 // 如下为reverse的主体实现
 2 template <class _BidirectionalIter>
 3 void __reverse(_BidirectionalIter __first, _BidirectionalIter __last,
 4                bidirectional_iterator_tag) {
 5   while (true)
 6     if (__first == __last || __first == --__last)
 7       return;
 8     else
 9       iter_swap(__first++, __last);
10 }
11  
12 template <class _RandomAccessIter>
13 void __reverse(_RandomAccessIter __first, _RandomAccessIter __last,
14                random_access_iterator_tag) {
15   while (__first < __last)
16     iter_swap(__first++, --__last);
17 }
18  
19 template <class _BidirectionalIter>
20 inline void reverse(_BidirectionalIter __first, _BidirectionalIter __last) {
21   __STL_REQUIRES(_BidirectionalIter, _Mutable_BidirectionalIterator);
22   __reverse(__first, __last, __ITERATOR_CATEGORY(__first));
23 }

回忆起在打开STL源码当时,恰如同小学时候解决完一道题之后去对照参考答案一样,本是怀着自信满满的喜悦之情,可参看答案之后喜悦之情荡然无存:“kao,又没有做对!”。在自己阅读STL当中reverse实现之后反生诸多疑惑:那个__STL_REQUIRES是什么东西?为什么要把reverse分成两层架构,之前自己的那个版本不行嘛?__ITERATOR_CATEGORY是干什么的?还有,那个__STL_CONVERTIBLE站在那里干嘛啊?天啦!还有一个__VALUE_TYPE!!!

省思录:生活似乎总是喜欢和人开玩笑,现实当中往往自己表现得非常乐观的时候,往往就是失望的前奏。真是应了鬼脚七常说的那句话:“没有期望,一切都是惊喜。”
 
对于上面几个问题,自己按照它们所映射的知识进行了简单划分,分别分成了I,II和III这三个问题,并分别进行解答。
 

问题I: 为什么STL中reverse的实现要进行分层实现?我写的version1.1版本不行吗?

对比两个版本当中的主要逻辑,似乎没有什么不一样的地方。对于随机迭代器的处理部分,完全可以复用双向迭代器的处理,为什么要一分为二?

 1 // 针对双向迭代器的处理
 2  
 3   while (true)
 4     if (__first == __last || __first == --__last)
 5       return;
 6     else
 7       iter_swap(__first++, __last); 
 8  
 9 // 针对随机访问迭代器的处理
10   while (__first < __last)
11     iter_swap(__first++, --__last); 

正着去想,随机访问迭代器的处理的确可以复用双向迭代器的部分,这的确是没有错的。不过这里暂且尝试换一种思路去想想:为什么STL的实现高手没有按照那样去做呢?​先假设STL当前这样的实现自有其考虑,那它们之间到底有什么不同?

 

详细对比每一条语句不难发现,针对随机访问迭代器的处理来得更高效。在两者的圈复杂度上,针对双向迭代器的实现比随机访问迭代器的实现大1,也因而相比之下它多了一倍的判断操作。因此,便可明白其分为两层架构实现的用意主要为了效率

 

这里或许有人会问,既然随机访问迭代器的处理高效,能不能让双向迭代器的处理也复用随机访问迭代器的处理呢?显然是不可以的,原因是对于双向迭代器来说,并不能保证__first < __last判断的有效性,这其实也是随机访问迭代器与其他分类迭代器的主要区别之一,可以拿链表与数组类比一下即可:)

 

综上,问题I 即可解答:version1.1版本是可行的,但却不是最优的实现,而STL的实现上效率是极其重要的一个考虑因素。

 
问题II: __ITERATOR_CATEGORY与__VALUE_TYPE是干什么的?
这个问题的解答不得不提到C++当中的traits编程技法,也揭露了version1.1版本的一个不足。上面已经提到了其第一个不足为效率低,那么这里要提到的第二个不足则是其可扩展性。version1.1版本当中使用了编译器的参数推导功能,从而可以在一个函数当中通过迭代器类型推算出它的value type,但按照上面的方式,对于一个返回值为迭代器指向元素类型的函数而言,编译器的参数推导功能是无法运作的。而这恰好也是C++ STL实现当中的traits编程技法的用武之地。
 
具体有关traits编程技法这里不展开,自己一言两语也是讲不清楚,在侯大师的《STL源码剖析》当中可知其来龙去脉。简单来说,__ITERATOR_CATEGORY和__VALUE_TYPE的产生都是traits机制当中的一种方法论的实现。__ITERATOR_CATEGORY借助了函数重载来使得算法支持不同迭代器,从而尽可能提高算法执行效率。而__VALUE_TYPE的产生则是解决上面的一个基本问题:“如何可以通过迭代器获知其value type”。 
 
问题III: __STL_REQUIRES与__STL_CONVERTIBLE有什么用?
这个,从何说起?在查看STL中reverse源码的开始就留意到这两个宏,两者的实现起初看起来显得晦涩难懂的,所以把它留到最后来解答。在这里有篇文章,从中可以得知这两个宏与STL当中的concept机制有关,你也可以在之前刘未鹏的一篇文章里面得到更多的信息 。
 
简单说来,concept机制是针对泛型编程的一种检测机制,用来帮助在模板编程时对各个参数提供有效性检查,相当于一种特别的assert机制。一起来看下__STL_REQUIRES与__STL_CONVERTIBLE的源代码吧:
 1 #define __STL_REQUIRES(__type_var, __concept) \
 2 do { \
 3   void (*__x)( __type_var ) = __concept##_concept_specification< __type_var >\
 4     ::__concept##_requirement_violation; __x = __x; } while (0)
 5  
 6 // Use this to check whether type X is convertible to type Y
 7 #define __STL_CONVERTIBLE(__type_x, __type_y) \
 8 do { \
 9   void (*__x)( __type_x , __type_y ) = _STL_CONVERT_ERROR< __type_x , \
10   __type_y >::__type_X_is_not_convertible_to_type_Y; \
11   __x = __x; } while (0)

以__STL_REQUIRES源代码为例,宏当中的第一个语句定义了一个函数指针__x,指向针对__concept当中的__type_var对象。接下来的__x = __x会触发编译-链接阶段__concept_concept_specification对应__type_var的实例化,在这个过程当中,一旦发现当前的__type_var并不能满足__concept的一些条件,这个时候就会出错(尽管报错的信息显得千百怪,但好歹也给予了一个错误提示)。__STL_CONVERTIBLE的基本原理大致也如此。自问自己当前对这一块还没有达到完全理解的程度,具体的原理留待日后分析。 

do...while(0)的技巧

在探究那些__STL_REQUIRES, __STL_CONVERTIBLE宏时,除了对宏本身不知其意之外,对于宏定义当中一再使用的do...while(0)式的语句同样也感到迷惑不解。所以,之后的一些工作也自然而然牵涉到do...while(0)。网上有几篇文章,可以参看这里这里这里,之后也便有拨开了云雾之感。自己依照这几篇文章,对有关do...while(0)的运用简要做了归纳,分为四种运用场景,记录如下。
 
场景一:简化程序控制结构
写代码时,在一个函数当中调用多个其他函数的情况并依照调用结果进行不同的逻辑处理是非常多见的使用场景。如下的情况或许你有见到过:
 1 int TestFunc()
 2 {
 3     somestruct *ptr = malloc (...);
 4  
 5     ...
 6     if (error)
 7     {
 8         free(ptr );
 9         return -1;
10     }
11     ...
12     if (error)
13     {
14         free(ptr );
15         return -1;
16     }
17     ...
18  
19     free(ptr );
20     return 0;
21 }

如上,代码中涉及到对多个函数调用的处理,当函数调用返回失败并且需要进行一些相同的操作,常见如释放内存。在这种情况下,如果针对每一个函数调用进行单独的处理就会显得冗余、臃肿不堪,这时可以略微改进之。

 1 int TestFunc()
 2 {
 3     somestruct *ptr = malloc (...);
 4  
 5     ...
 6     if (error)
 7         goto ErrorCatch;
 8     ...
 9     if (error)
10         goto ErrorCatch;
11     ...
12     free(ptr );
13     return 0;
14  
15 ErrorCatch:
16     free(ptr );
17     return -1;
18 }

上述的代码在结构上面显得工整和整洁,但不幸的是,它使用了goto语句,而goto语句在软件工程学当中是一直被众人所诟病、不建议使用的(尽管我都还没有爽过-_-)。而这个时候使用do...while(0)语句进行优化就显得那么的“柳暗花明”了。

 1 int TestFunc()
 2 {
 3     somestruct *ptr = malloc (...);
 4  
 5     ...
 6     do
 7     {
 8     if (error)
 9         break;
10     ...
11     if (error)
12         break;
13     ...
14  
15     free(ptr );
16     return 0;
17 }while(0)
18  
19     free(ptr );
20     return -1;
21 }

 场景二:定义复杂的宏

提到宏,在我们脑海里面立即浮现出来的可能更多的是将之与typedef,函数相比较之类的面试题目;在平常项目当中使用的可能也是非常简单的宏定义。而在STL源码当中、linux内核等开源项目当中如下的用法着实会让人眼前一亮。
 
1 #define __STL_REQUIRES(__type_var, __concept) \
2 do { \
3   void (*__x)( __type_var ) = __concept##_concept_specification< __type_var >\
4     ::__concept##_requirement_violation; __x = __x; } while (0)

如前所说,平时使用到的宏,通常只见单个语句,而如上的宏定义当中则包括了两条执行语句。这里先撇开do...while(0)不看,我们可以试着去想一想,对于定义一个包含多个语句的宏,我们该如何去完成它?如下是我自己的思考过程,挺傻瓜,不过有利于自己的理解。 

1)首先:使用最宏基本的定义方式,因为考虑到if等判断语句的使用场景,必须将两条语句放在一起,所以给它们都加上括号(自认为是一种聪明的做法)。
#define MICRO_FUNC(arg1, arg2) (statement1; statement2)
#define MICRO_FUNC(arg1, arg2) (statement1; statement2;)
 
这种定义编译会不通过,因为C的语法当中没有在括号之间使用分号';'的情况。
 
2)修改:使用{}将两条语句粘合在一起。
#define MICRO_FUNC(arg1, arg2) {statement1; statement2;}
 
这样看起来的确解决了问题,但是在碰到if...else...语句的时候,又将出现问题。比如
1 if (true)
2     MICRO_FUNC(arg1, arg2); // C/C++当中规定分号作为一行语句的结束符号,所以一直以来的编码习惯上,我们都会在每条语句后面加上分号。
3 else
4     ...

这个时候宏展开将会是这样子的情况:

1 if (true)
2     {statement1; statement2;}; // 这里将多了一个分号,这个分号便会承接if条件不满足时的处理,也就意味着if语句的范围到这里结束
3 else
4     ...

此时,else语句掉空,无法通过编译。

||=== Build: Debug in TestVariableDeclaration (compiler: GNU GCC Compiler) ===|
F:\Coding\C\TestVariableDeclaration\main.c||In function 'main':|
F:\Coding\C\TestVariableDeclaration\main.c|23|error:
 'else' without a previous 'if'|
||=== Build failed: 1 error(s), 0 warning(s) (0 minute(s), 0 second(s)) ===|
 
当前这种情况的主要原因是多了一个分号,那么在宏定义当中的statement2后面不要加分号不就行了? 如果谁真想到这种改造方式该拍板子咯(我是第一个尝板子的人-_-),这个行不通的,C/C++最基本的语法概念。
 
到这里,就会发现,将do...while(0)语句来对宏进行改造,来得多么漂亮,又多么顺理成章!
#define MICRO_FUNC(arg1, arg2) do{statement1; statement2;}while(0)
 
场景三:消除空宏引起的警告
1 SGI-STL3.3当中的代码片段
2 // Some compilers lack the features that are necessary for concept checks.
3 // On those compilers we define the concept check macros to do nothing.
4 #define __STL_REQUIRES(__type_var, __concept)
5  do {} while(0)
linux-3.3.1 当中的代码片段

static void check_spinlock_acquired_node(struct kmem_cache *cachep, int node)
{
#ifdef CONFIG_SMP
 check_irq_off();
 assert_spin_locked(&cachep->nodelists[node]->list_lock);
#endif
}
 
#else
#define check_irq_off() do { } while(0)
#define check_irq_on() do { } while(0)
#define check_spinlock_acquired(x)  do { } while(0)
#define check_spinlock_acquired_node(x, y)  do { } while(0)
#endif

在平常的项目当中,或许极少使用过空宏的情况,但它确实存在。比如STL源码当中一些编译器不支持concept check,在内核代码保持对不同体系结构兼容时也有需要使用到空宏的地方(见如上两个代码片段)。对于简单的定义一个宏的值为空,编译器都会提示warning。而do...while(0)语句可以用于消除这些warning。 

场景四:优化代码组织结构,向C98兼容
C89规定了变量的声明只能够在语句块的开头声明,因此对于当前一些旧的仅支持C89的编译器而言,如下的方式是错误的。
 1 int main() 
 2 {
 3     int ret = TEST_INIT_VALUE;
 4     printf("ret = %d\n", ret); 
 5 
 6     int val = 0; // 错误。
 7     DoSomethingToY(&val);
 8     ret = ret + val; 
 9 
10     return ret; 
11 }

C99时代,标准中新增了对混合变量的声明(有关C99新增特性的介绍,这篇文章介绍得比较通俗),尽管如此,如果需要考虑到移植性或者在特定的情况需要如此(比如当前编译器版本仅支持C89),使用do...while(0)进行修改是一个不错的方法。 

 1 int main() 
 2 {
 3     int ret = TEST_INIT_VALUE;
 4     printf("ret = %d\n", ret);
 5  
 6     do
 7     {
 8         int val = 0;
 9         DoSomethingToY(&val);
10         ret = ret + val;
11     }while(0);
12  
13     return ret;
14 }

 

正文完。本篇笔记从一个reverse函数的实现开始,先后涉及到了C++当中的traits机制和concept checking机制,再到对do...while(0)技巧的简要归纳,自以为学有所得,也挺觉得心满意足。予以分享,希望有用:)