在常识成为常识之前:细读Ruminations on C++

the ideas in this book hava become more important with time

Ruminations on C++ 是由Andrew Koenig 与Bjarne Stroustrup写的,贝尔实验室成员,1986年从事C语言研究,还有一本书是《C缺陷与陷阱》。

图1:ruminations on c++

图2:ruminations on c++

简单来说,本书就是强调了一个理念。认字最多的人一定是最好的作家吗?不是,讲好故事才是一个好作家的标配。The hard part of programming is not learning the details of language features——it is understanding how to solve problems. 所以编程中最困难的地方不是去学习语言的细节,而是理解问题的解决之道。

书中主要表达了,

C++最基本的设计理念就是"用类来表示概念";

C++解决复杂性的基本原则是抽象;

面向对象思想是C++的手段之一,而不是全部。

所谓面向对象编程,就是使用继承和动态绑定机制编程。也就是两个及以上的类型,至少有一个共同操作,也至少有一个不同操作,否则就不需要继承。其次,必然有一个场景,需要在运行时从这些类型中挑选一个,否则也不需要动态绑定机制。

继承让我们抓住了各种节点类型之间的相似之处,而动态绑定帮助我们为各种类型节点定义操作,让编译器来负责安排在运行时能够调用正确的函数。

C++在运行时性能上做了一个很好的折中,能够在"一切都是对象"的语言与"避免任何抽象"的语言之间取得恰到好处的平衡,这就是C++的实用性。

从语言本身来说,C++是挺"温柔"的,他敏锐、高效、成熟、稳重、分寸感很强、最重要的是"自主"。

挑几个比较有趣的章节讲一下

1.Bjarne Stroustrup 在The C++ programming Language(1991)提到的定义句柄类,简单来说,就是将引用计数从数据中分离出来,把引用计数放入它自己的对象中。

得简单介绍下句柄,它允许在保持代理的多态行为的同时,避免进行不必要的复制。

图3:ruminations on c++

handle应该"控制"它所绑定到的对象,创建和销毁。从效果上来说,handle就是一种只包含单个对象的容器。

图4:ruminations on c++

因为使用上通常是复制句柄,我们必须了解有多少个句柄绑定在同一个对象上,只有这样才能确定应当在何时删除对象。而通常使用引用计数(use count)来达到目的。所以设计了定义一个新的类,来容纳一个引用计数和一个Point对象。

但这种技术有一种缺点,为了把句柄捆绑到类T的对象上,必须定义一个具有类型为T的成员的新类,较为繁琐。使用定义句柄类会更加简单。

图5:ruminations on c++

图6:ruminations on c++

将引用计数从Point里面分离后,便会改变对句柄类的实现。不再有指向UPint的指针,此时只有指向Point的指针和指向一个int的指针表示引用计数。然后再定义一个新类即可。然后再抽象一下即可。

图7:ruminations on c++

最后就是将引用计数类当成句柄实现的一部分,与各种不同的数据结构协同工作,简化了引用计算句柄实现,使得handle类与其他传统方式实现的类同样容易使用,且更加灵活。

图8:ruminations on c++

2.Generic iterators 泛型迭代器

1
2
3
4
5
6
template<class P, class T>
P find(P start, P beyond, const T& x){
    while (start != beyond && * start != x)
      ++start;
    return start;
}

这里面定义了类P,使用了同一个find函数来对数组、链表、文件或者其他的数据结构进行查找,经典的模板编写,适用于不同的数据结构和参数类型的函数。

理解了这个,就深入一下,探究迭代器类型和算法之间的关系。

首先我们对迭代器进行分类。

1.输入迭代器,满足这些需求的类型允许我们按照某个预定的顺序去读,序列中的元素。这个输入迭代器,既不是某个特殊类型也不是对象。

图9:ruminations on c++

2.输出迭代器,就是对序列中进行写操作。

图10:ruminations on c++

这时候,可以用输入和输出迭代器写一个泛型复制函数了.

1
2
3
4
5
6
7
8
template <class In,  class Out>
void copy(In start , In beyond, Out result){
     while (start != beyond){
       *result = *start;
       ++result;
       ++start;
       }
}

上面比较容易理解,不过一般都是写成

1
    *result++ = *start++;

3.前向 图11:ruminations on c++

4.双向 图12:ruminations on c++

5.随机 图13:ruminations on c++

譬如说

1
P mid = low + (high - low) / 2;

迭代器在性能上是有所保障的,譬如说一个双向迭代器从序列的第n个元素退后到第n-1个元素的时间复杂度是O(n)。但STL赋予迭代器的支持,分摊执行时间必须是O(1)。

所谓泛型算法就是:对于所操作的数据结构的细节信息,只加入最低限度的了解。甚至是不需要。

通过建立一些指针,来分析一下

1
2
3
4
5
6
template<class P, class T>
P find(P start, P beyond, const T& x){
    while (start != beyond && * start != x)
      ++start;
    return start;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int main()
{
  char* hello = "Hello ";
  char* world = "world";
  char message[15]
  char* p = message;
  
  p = copy(hello,hello+6,p)
  p = copy(world,world+5,p)
  *p = '\0';
  
  cout << message << endl;
 }

就是被迭代器值start和end限定的序列复制到一个以迭代器dest为开始位置的序列中,对p*的赋值在d后面放入空字符(c的要求)。

加一点理解。

如果我们想要找某个特定值相等的最后一个,而不是通常的第一个。

可以使用双向迭代器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
templeate<class T, class X>
T rfind(T start, T beyond, const X & x){
  if (start != beyond){
    T p = beyond;
    do(
       if(*--p == x)
         return p;
       )while (p != start)
    
      return beyond;
}

还可以再深入一点。

直接自动反向。

创建一个新类型TR,使用"邻接技术"将T转换为TR,并且保障++和–都能使用,以及常规的复制、赋值、比较和解除dereference都能使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

// C++
template<class It,class T> class Rev{
  friend bool operator == 
    (const Rev<It, T>, const Rev<It, T>;
  friend bool operater == 
    (const Rev<It, T>, const Rev<It, T>);
    
 public Rev(){}
        Rev(It,i): it(i) {}
        operator It() {return it;}
        
        Rev<It,T>& operator++() {--it; return *this;}
        Rev<It, T> r = *this;
        --it;
        return r;
        
        Rev<It, T> & operator-(int){
              Rev<It, T> r = *this;
              ++it;
              return r;
        }
        
        T& operator*() {
              It I = it;
              -- i;
              return *i;
        }
 private:
   It it;
 };
 
 template<class It, class T>
 bool operate == (const Rev<It,T>& x, const Rev<It, T> &y)
 {
       return x.it == y.it;
 }
 
 template<class It, class T>
 bool operate != (const Rev<It,T>& x. const Rev<It,T> &y)
 {
       return x.it != y.it;
 }

然后,我们就可以随便找到第一次出现的位置,和最后一次出现的位置。

1
2
3
Rev<int*, int> r =   
      find(Rev<int*. int> (x + 310),
      Rev<int*,int>(x),9);

迭代器配接器可以极大减少要写的算法数量,达到事半功倍的效果。

差不多就谈到这儿了。

这是在一个新语言的创造中心里,思考与实践的Koenig写的书,也就是常识成为常识之前(hhh点题了)。Bjarne,Rob Murray,Martin Carrcoll,Barbara Moo与Koenig等这些人一起开创了C++时代,成为一个强大的、稳健的、成熟的高级语言。

这也就是本书的意义所在,要知其然,也更要知其所以然。但它带来的思考不仅仅是这些。历史总与现在遥遥相合,在这个时代里创造新的技术,也要走同样的路的。

用Koenig的话来作为结束吧,

As a result, the ideas in this book hava become more important with time.

时光如筛,必现所金。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus