打印
[LOOK]

李老师群课笔记之元组

[复制链接]
1974|3
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 mahui843 于 2013-7-7 13:13 编辑

李老师群课笔记之元组
主讲: 李老师(John Lee
元组(tuple)
  C没有把数据和代码作为整体抽象,还有泛型支持,衍生而出的元编程,这些都是C无法做到的。
要同时满足“效率”和“扩展性”两方面的要求,达到“优雅”的境界,目前好像只有C++能够做到。
先介绍一个C++标准库的一个组件:tuple,这个东西相当有趣,中文翻译大概是“元组”。
tuple类似于数组和struct的混合体, 像数组,是因为tuple有很多元素。像struct,是因为tuple的各个元素可以不同类型。
例如:
tuple<int, char, float>bar;
tuple本身是一个类模板.
如果要访问第0个元素,可以用数组下标的形式:get<0>(bar)
要注意,因为是元编程,所以下标必须是常数
例如,给bar的第0个数据(那个int)赋值:
get<0>(bar) = 5
取数据:
int i = get<0>(bar);
第1个元素(char)
get<1>(bar) = 'a';
char ch = get<1>(bar);
tuple的内存布局,跟struct基本相同,没有任何附加数据。
tuple除了可以读写其中的元素外,还可以取得某个元素的类型,还可以取得整个tuple的元素个数
取元素的类型用tuple_element模板:
tuple_element<0, decltype(bar)>::type
这就可以取到0号元素的类型,就是那个int ,取到类型后,可以用这个类型去定义新的变量等等.
取元素个数,用tuple_size模板:
tuple_size<decltype(bar)>::value
好了tuple介绍到这里。那么tuple具体有什么用呢?
我们来把下午说的功能扩展的问题,抽象一下:
很显然,一个“扩展功能”,需要用一个code来代表,还有一个函数。
那么我们可以抽象出这样类来表示:
struct A {
    static const uint8_t code = 1;   //
功能代码
    void function();            // 功能函数

    ....                             // 其它类成员数据或函数
};
好,再来一个功能代码为3的(故意不连续):
struct B {
    static const uint8_t code = 3;
    void function();
    ....
};
为什么codestatic const呢?
C++规定,类中的静态常量,不占存储,并且可以用类名访问,而不依赖某对象。
相当于在类中define了一个符号。
那么,我们访问code的时候,就可以用A::code,B::code等等去访问。
tuple把这些定义好的功能类,全部包含起来。
这些功能类,只是执行方,它们还需要一个管理者来管理。
在以前的C中,就是那些if else switch,这个是关键,效率和扩展性的完美结合,就看这个管理者类了。这个管理者,肯定是一个类模板。它的模板参数就是那些功能类。
先给出它的原型定义:
template<typename... FUNCTIONS>
struct dispatcher
为了简单起见,我都没有用class定义类和模板。
好了,刚才说到tuple出场
那么现在就请出tuple:
template<typename... FUNCTIONS>
struct dispatcher : std::tuple<FUNCTIONS...> {
     ....
};
这样,各个功能类,就全部包含到tuple中了。
并且,这个管理者(dispatcher)也继承于tuple,那么就是说,所有的功能类,都被包含到了dispatcher.
还记得mixin吗?
整个程序,就是一个一个的结构互相融合,最后都集中到了一起.
现在我们需要做code的比较和对应功能的调用工作了。
这个要用到类的特化。
先把整个dispatcher的定义给出:
template<typename... FUNCTIONS>
struct dispatcher : std::tuple<FUNCTIONS...> {
    void do_function(uintptr_t code) {
helper<>::do_function(*this, code);
}
    template<typename T = void, uintptr_t N = 0>
    struct helper {
        static void do_function(dispatcher& rz, uintptr_t code)
        {
            if (std::tuple_element<N, typename dispatcher::tuple>::type::code ==  code)
                std::get<N>(rz).function();
            else
                helper<T, N + 1>::do_function(rz, code);
        }
    };
    template<typename T>
    struct helper<T, std::tuple_size<typename dispatcher::tuple>::value> {
        static void do_function(dispatcher&, uintptr_t)
        {
        }
    };
    ....
};


第2行是用tuple把各个功能类包含起来,并作为dispatcher的基类
从第6行开始,定义了两个类模板,一个是非特化,一个是特化。
这两个类模板,是定义在了dispatcher的内部,C++称为嵌套类模板。
这个都关系不大。
类模板有两个参数<typenameT, uintptr_t N>
说到helper模板有两个带默认值的模板参数,重点在第2个模板参数uintptr_t N ,这个的作用是用于从tuple里取元素时用到的下标,
我还是先说说程序的思路吧:
程序收到了code后,在tuple里从0号元素开始,直到最后一个元素结束,一个一个地取出这些元素(功能类),有点像for循环了,然后用每一个功能类里定义的静态常量code值,与实际需要执行的code比较,如果相等,那么就知道了该功能类在tuple中的常量下标了。然后用这个下标去取出tuple中该功能类的对象,并调用该对象的function函数。
不要忘了,全部的功能类,都在tuple中有对象的。
好了,我们来具体推演一个code的dispatch过程
首先,dispatcher模板的do_function函数被外部调用,并传来一个code参数(变量),
void do_function(uintptr_t code) {
       helper<>::do_function(*this, code);
    }
这个do_function直接调用了helper嵌套类模板里的do_function函数
注意这里的helper模板,没有给出参数,只有一对空的<>
刚才说了,helper类模板的参数有默认值,那么这个空的helper<>,意思就是使用其参数的默认值 :T = void ;这个T没有什么实际用处,只是占个位置,具体原因稍后再讲。
好了,现在随着dispatcher::do_function的调用,我们来到了helper<void, 0>::do_function函数。
这个函数接收了两个实参:
1dispatcher类的引用
2、实际需要执行的code
template<typename T = void, uintptr_tN = 0>
   struct helper {
       static void do_function(dispatcher& rz, uintptr_t code)
       {
            if (std::tuple_element<N,typename dispatcher::tuple>::type::code == code)
               std::get<N>(rz).function();
            else
                helper<T, N +1>::do_function(rz, code);
       }
};
在该函数里,首先使用tuple_element模板根据 N(下标)从 tuple中取出了对应的功能类,注意,这里是功能类的“类型”,而不是对象。
得到了类型后,从类型中取出“静态常量code与第2实参code比较。
如果相等,那么N下标所代表的tuple中的这个功能类,就是我们需要执行的。接下来,就使用getdispatcher的引用rz中取出N下标代表的功能类“对象”。
    再提醒一下,由于dispatchertuple的派生类,所以对dispatcher使用get,就等于对tuple使用get
取得了对象后,就直接调用该对象的function函数,完成具体功能。
如果code不相等,那么就调用helper<T, N + 1>do_function函数
在调用时,编译器先对 N +1,由于N是常量,所以 N + 1仍然是常量。现在调用的helper,就是helper<void, 1>了,刚才N 是0,那么这句推演的结果是:helper<void, 1>::do_function(rz, code); C++在每次推演模板时,都要匹配模板和该模板的特化版本。
    我们来看看这个helper模板的特化版本:
   template<typename T>
   struct helper<T, std::tuple_size<typename dispatcher::tuple>::value>{
       static void do_function(dispatcher&, uintptr_t)
       {
       }
   };
这个特化版本的T参数是没有特化的
它特化了第2参数N
使用的是
tuple_size模板计算出的tuple元素个数。
刚才我们把功能类 A B 加入了 dispatcher(也就是tuple),那么这个std::tuple_size<typename dispatcher::tuple>::value 显然就是常量2。
好了我们推演这个特化版本的helper的结果是:
   template<typename T>
   struct helper<T, 2> {
       static void do_function(dispatcher&, uintptr_t)
       {
       }
   };
回到刚才的调用 helper<void, 1>::do_function(sz, code)
这个helper<void, 1>
类型,显然不等于helper<void, 2>类型 。
这表示helper的特化版本匹配不成功,编译器就会继续调用helper的非特化版本的do_function函数。这个就是所谓的编译时递归了。直到N + 1 == 2时,编译器发现与helper的特化版本匹配了,就去调用特化版本中的do_function函数
我们把推演全部展开一下(伪码):

if (A::code == code)
   A(sz).function();
else if (B::code == code)
   B(sz).function();
如果两次比较都不匹配,那么就到了特化版本的do_function什么都不做,忽略之。当然,你也可以在特化的helper::do_function里执行一些设计好的默认动作,比如发回一个NACK等等。
整个过程分析完了,我们来看看它的效率:

相关帖子

沙发
mahui843|  楼主 | 2013-7-7 13:12 | 只看该作者
本帖最后由 mahui843 于 2013-7-7 13:14 编辑

贴出整个程序:
#include <cstdint>
#include <tuple>
template<typename... FUNCTIONS>
struct dispatcher : std::tuple<FUNCTIONS...> {
    void do_function(uintptr_t code) {
        helper<>::do_function(*this, code);
    }
    template<typename T = void, uintptr_t N = 0>
    struct helper {
        static void do_function(dispatcher& rz, uintptr_t code)
        {
            if (std::tuple_element<N, typename dispatcher::tuple>::type::code ==  code)
                std::get<N>(rz).function();
            else
                helper<T, N + 1>::do_function(rz, code);
        }
    };
    template<typename T>
    struct helper<T, std::tuple_size<typename dispatcher::tuple>::value> {
        static void do_function(dispatcher&, uintptr_t)
        {
        }
    };
    int x;
};
struct A {
    static const uint8_t code = 1;
    void function();
    int x;
};
struct B {
    static const uint8_t code = 3;
    void function();
    int x;
};
dispatcher<A, B> z;
void foo(uintptr_t code)
{
    z.do_function(code);
}
struct A 和 B 是两个功能类,为了防止空类,我特意加了一个 int x;
实际的功能类中,肯定会有些数据成员的。
倒数第4行,是用dispatcher模板加上了两个功能类参数,实例化了实际的类dispatcher<A, B>,并使用这个类定义了一个对象 z 。然后是一个引子函数foo,是为了调用 z.do_function,我们来看看编译结果 :
  
这个是foo函数的反汇编
r0是参数code
先比较了r0和常数1,就是A::code
不等就跳转到 10
bne.n 10
相等,就装入 z 对象的地址:
ldr r0,[pc, #20]
并加4,得到A类在dispatcher中的偏移:
adds r0,#4
然后就调用了A::function
调用完后,就b.n 1a 返回了
如果不等于1,跳转到10后,接着比较#3
那是B::code的值,如果相等,取得B在dispatcher中的偏移后,调用B::function,然后返回 ,如果不等于#3,直接跳转到1a返回:
bne.n 1a
整个汇编代码,一句废话没有
现在,我来扩展一下,定义一个新的类:
struct C {
     static const uint8_t code = 100;
     void function();
     int x;
};


这个code是100
编译
然后把这个类简单地加入dispatcher
dispatcher<A, B, C> z;
看看刚才的dispatcher,用户只需要定义他的功能类,加入dispatcher就行了,至于dispatcher里干了些啥,一概不知,也不用知。
这回依次比较了#1,#3,#100
各个功能类可以分别定义,做到“闭原则”,而使用时,可以很容易的扩展,做到“开原则”,而且是最高效率的。
而核心,只需要定义好代码和数据的管理和分派。到时候,mixin就自然发挥威力了。随意组合功能 。

使用特权

评论回复
板凳
呆板书生| | 2013-7-7 13:17 | 只看该作者
标志一下,慢慢学习

使用特权

评论回复
地板
缥缈九哥| | 2013-7-7 13:19 | 只看该作者
顶起

使用特权

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

本版积分规则

个人签名:境有界  思无域

6

主题

63

帖子

1

粉丝