打印
[LOOK]

李老师群课笔记之基于开闭原则的C++模板程序的CRTP程序框架

[复制链接]
3306|12
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
mahui843|  楼主 | 2013-6-30 22:19 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
授课:John Lee   整理:缥缈九哥
     通俗的说“开闭原则”,就是写过的代码,不能再修改。要新的功能,只能添加,除非纠bug,或者重构之类。
那么只能添加,如何做?   
   从基类派生啊
   接口尽量考虑周到
   继承
多开闭原则的定义倡导对抽象基类的继承。接口规约可以通过继承来重用,但是实现不必重用
在面向对象之类的语言中,比如已存在的某个功能,想扩展一下,请看看下面的例子。
大家都知道虚函数的作用吧?
用来扩展的
派生类中如果不重新定义虚函数,则调用基类定义的虚函数
它是用来扩展的,虚函数就是C++实现“开闭原则”的“开”的机制,“闭”就如上撰述。虚函数的开销还是一些的。你们发现look里有没有虚函数?数量不多,开销还可以承受。
    用Mixin,
    怎么扩展都行。把功能块写好,再组装。
    如果功能在模块自己呢?自己如何组装?虚函数是一种方法,可是粒度太细,开销太大,而且粒度越细,开销越大,
    mixin的另外一种实现机制,是CRTP,就是JamesCoplien在1995年提出的奇异递归模板模式(CRTP)。
    CRTP和虚函数各有优缺点,虚函数的优点是基类不需要知道派生类的信息,只管调用虚函数就是,这个得益于虚函数的“晚捆绑”实现机制。
    虚函数的缺点就是“晚捆绑”要一定的空间开销和运行开销。
而CRTP正好相反,它不使用“晚捆绑”机制,因此没有这些开销,但缺点是基类需要知道派生类的信息,才能调用派生类的函数。但是派生类是不可预料的。而CRTP模式,正是为了把派生类信息传递给基类。正因为是编译绑定才没有多余的开销。CRTP只是看起来比较怪异,可以看这个例子:
https://bbs.21ic.com/icview-551444-1-1.html
    奇异递归模板模式(CRTP)
自身扩展,是mixin的一个重要方面,而传统的OOP,是借助虚函数实现的。继承基类,覆盖虚函数,就完成了自身扩展。
自身扩展,就是从基类的接口(函数)进入,调用到了派生类的函数。如果不把派生类的信息传递到基类,基类如何调用派生类的函数?
  基类为何要调用派生类的函数?
派生类自己调用不能实现功能吗?
这时派生类都还没写出来呢,基类不能调用派生类的函数。实现功能的入口(起点)是在基类,而中间部分实现可能有变化(可以扩展)的。
可是派生类的存在不就是为了对对象进行封装和扩展吗,如果让基类调用派生类,不是破坏了封装吗
那你还没有理解虚函数的真正意义
虚函数的真正有用的,是用基类指针调用虚函数。如果已经知道了派生类,那么虚函数是无意义的。
从派生类中调用基类的函数,是很普通的,因为派生类就包含了基类的信息,编译器可以使用早捆绑。
但如果扩展都必须从派生类作为起点,那么这种所谓的“扩展”没有什么意义
通常的做法就是从基类开始一层一层的派生,一直到最派生类,层层的继承和扩展
一层一层打包
    比如模块A(class A),与模块B(class B)聚合,class A调用class B的某个功能,两者没有is-a的关系,就是class A中的一个函数,调用class B中的一个函数。现在你想修改一下功能。但根据“闭”原则,你不能修改 class B。那么你从class B派生了class C来实现修改的功能。但是,你拿着class C 去跟class A说,这是新功能,你要用class C。class A能认识class C 吗?
不认识!class A只认识class B。所以,你的class C要装成 class B,去“骗”class A。那么class A 如何能通过class B访问到class C呢?
因为class C是class B的派生类,所以class C已经有了class B的全部信息,所以当class A遇到class C时,它还是认为遇到的是class B。
如果你在class C中覆盖了这个功能(函数),但class B中的原函数没有定义为虚函数,那么显然,class C中的函数,和class B中的函数不是一回事,即使它们同名。class A仍然会调用class B(现在它是class C的基类)中的原来的函数。
其实class C有两个函数(同名的),分别位于基类(class B)和派生类(class C)。
因为作用域的不同,所以不会因为同名同参数引起二义性?
        例如:
       struct B {
          void foo();
       };
      
       struct C {
          void foo();
       };
      
       C c;
      
       B* p = &c;
       p->foo();        
       上面的p->foo()调用哪个?是调用B::foo()?还是C::foo()?
    它会调用B::foo(),虽然是C类型的变量c,但赋值给B类型的指针p时,就发生了up cast,类型收窄了。
当用p指针调用foo()时,因为p的类型是B*,所以会调用到B::foo()。
所以,如果class B的函数没有定义为虚函数的话,那么该功能是没有留出扩展的余地。
你即使派生了class C并写了同名的函数,也没有任何作用。class A仍然只认B::foo()。最后访问的还是基类的成员函数。
实际使用虚函数时,绝大多数是这种情况,几乎100%的。classA在不认识class C的情况下,能够调用到class C的函数,是C++“晚捆绑”机制实现了这种效果。
那纯虚函数的意义又何在?只是为了实现统一的接口吗?最后派生类中都要实现纯虚函数的啊,干脆直接在派生类中定义不是一样的吗
那就是剩下有没有“默认处理”的问题了。有,就不用纯虚函数,没有就用。但是有些功能,有很多种实现,但不好让某一种做成默认,所以就让它空着。比如look的调度器。
  Protected:
       virtual task_t* dispatch() = 0;
       vritual task_t* ready(sync_t&sync,task_t* task = 0) = 0;
       vritual void block(sync_t& sync) = 0;
    look有几种调度算法,但不好说哪种能够做成默认调度算法。那样有失偏颇。所以就做成了纯虚,必须要用户选择一个派生类(调度算法)
    回到CRTP,由于CRTP不使用虚函数,那么让基类调用派生类函数的唯一方法,就是让基类知道派生类的信息。调用时,使用down cast,把基类强制成派生类,就可以调用派生类的函数了。如何将派生类的信息传递给基类呢?正确的答案是,把基类做成类模板,
模板的一个参数,就是派生类。实例化时就有了正确的信息。
    template<typenameT>
    class A {
    public:
      void foo() {
          static_cast<T*>(this)->bar();
       }
      void bar() {
         ....
       }
    };
   
    class B : public A<B> {
    public:
       void bar() {
           ...
        }
    };
    typename T就是派生类,在定义是只是一个模板参数,而当定义classB时,继承了class A,并把 B 作为模板参数传递到了class A。
在调用foo()时,实例化后的情况是:
    void foo() {
      static_cast<B*>(this)->bar()
    }
由于A是B的基类,所以down cast(static_cast)会成功。这就保证的安全性。
模板参数 是可以任意类型吗?
不是派生类,不让down cast。
    class C : publicA<C> {
         ...
    };
   
    C c;
    c.foo(); // 调用了bar吗?
   它调用了哪个bar?在class C中,没有定义bar函数。C没有定义foo,我们再来实例化一下:
    void foo() {
       static_cast<C*>(this)->bar();
    }
    A是C的基类,如果一个函数在派生类中找不到,那么自动调用基类的函数。这就有点虚函数的味道了。
定义了,就被调用,不定义,就使用基类函数。关键是,这是编译时捆绑的,没有多余开销。
    有了CRTP,你可以把一个功能分成很细的粒度,每步都做成down cast调用,并且每步都有默认处理(inline的),用户想在哪步修改逻辑,就把哪步抓出来覆盖。这个是虚函数不敢想的。就像一个链条,每步都可以修改。
    例子:
    template<typename T>
    struct A {
      void function() {
          static_cast<T*>(this)->major();
       }
      void major() {
          static_cast<T*>(this)->minor1();
          static_cast<T*>(this)->minor2();
       }
      void minor1() {
          static_cast<T*>(this)->step1();
          static_cast<T*>(this)->step2();
          static_cast<T*>(this)->step3();
       }
      void minor2() {
          static_cast<T*>(this)->step4();
          static_cast<T*>(this)->step5();
          static_cast<T*>(this)->step6();
       }
      void step1() { ... }
      void step2() { ... }
      void step3() { ... }
      void step4() { ... }
      void step5() { ... }
      void step6() { ... }
    };
    function()是整个功能,里面调用了major,major的默认处理调用了minor1,minor2。minor1, minor2 分别调用了各个step,
这就是整个骨架,你可以抓出某个step改写,也可以抓出minor改写,甚至抓出major改写。控制粒度可以从“整个”到“极细”。
A只是提供一套CRTP给你改写“皮肤”,“肌肉”,“骨架”的规则。
    可以再看看这个USB实现的mixin withCRTP 的复杂系统例子:
   http://code.google.com/p/lestl/source/browse/usbd/core.hpp
(下课....)

相关帖子

沙发
缥缈九哥| | 2013-6-30 22:21 | 只看该作者
顶起

使用特权

评论回复
评分
参与人数 1威望 +2 收起 理由
mahui843 + 2
板凳
mahui843|  楼主 | 2013-6-30 22:21 | 只看该作者
沙发占掉

使用特权

评论回复
地板
mahui843|  楼主 | 2013-6-30 22:23 | 只看该作者
没抢到沙发,九哥快了一步

使用特权

评论回复
5
liumingxing| | 2013-6-30 22:26 | 只看该作者
顶起

使用特权

评论回复
评分
参与人数 1威望 +2 收起 理由
mahui843 + 2
6
xukaiming| | 2013-6-30 22:27 | 只看该作者
:)加点精

使用特权

评论回复
评分
参与人数 1威望 +2 收起 理由
mahui843 + 2
7
wjsjdeng| | 2013-6-30 22:29 | 只看该作者
顶起,,哈哈也占个位以后好查看

使用特权

评论回复
评分
参与人数 1威望 +2 收起 理由
mahui843 + 2
8
dong_abc| | 2013-6-30 23:09 | 只看该作者
本帖最后由 dong_abc 于 2013-7-1 01:35 编辑

开闭原则的扩展两种实现方式: 虚函数/CRTP

使用特权

评论回复
评分
参与人数 1威望 +2 收起 理由
mahui843 + 2
9
ceflsh| | 2013-8-15 19:59 | 只看该作者
学习了,顶一下。

使用特权

评论回复
评分
参与人数 1威望 +2 收起 理由
mahui843 + 2
10
yong61| | 2013-9-4 18:47 | 只看该作者
有些用处

使用特权

评论回复
11
bingxinjian226| | 2013-11-1 14:28 | 只看该作者
看不懂~只会C语言

使用特权

评论回复
12
bingxinjian226| | 2013-11-1 14:29 | 只看该作者
有时间补充下~C++

使用特权

评论回复
13
szjosun| | 2013-11-1 15:00 | 只看该作者

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:境有界  思无域

6

主题

63

帖子

1

粉丝