本帖最后由 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();
....
}; 为什么code是static 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函数。 这个函数接收了两个实参: 1、dispatcher类的引用 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中的这个功能类,就是我们需要执行的。接下来,就使用get从dispatcher的引用rz中取出N下标代表的功能类“对象”。 再提醒一下,由于dispatcher是tuple的派生类,所以对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等等。 整个过程分析完了,我们来看看它的效率:
|