主讲 李老师(John Lee) Meta Programming真是个可怕的东西,把觉得不可能实现的东西实现了。 编程就是编写计算机程序来编写或操作其它程序(或它自己)作为它们的数据,或者是在编译时而不是运行时干一部分工作。大多数情况下,在同一时间里程序员使用元编程比手写代码更有效率。 元编程非常奇妙。普通的编程,处理的是数据,而类型在编程时就定死了。元编程的处理对象,则是类型。很多时候,编程时定义某数据的类型,不一定是最佳,比如数组。 在普通的程序设计里,你必须给数组一个无条件的、明确的类型,比如uint16_t等等。但最终你可能发现,实际运行时,数组的每个数据都不超过uint8_t。以致于不同的类型要写类似的代码。也可能浪费了一半的存储。如果你很在乎节省那些存储,你可以在编完程序后,回头来把数组类型改成uint8_t。 做个宏定义 ? 做个条件编译的宏?宏定义也是要“定义的”。 很多时候,数组的大小,不可能简单地知道。特别是这个数组的size,需要根据分布在各个模块中的其它数据或参数,综合计算得出。宏来做这件事,几乎无法达成。 然后应该怎么做?然后就是,元编程可以轻松的做到。 元数据怎么做?重点在于struct/class中的常量。 struct中可以定义静态常量和类型,这个是关键: struct A { static const bool value = ..... }; value可以不依赖A的对象,而只需要对A类访问就可以A::value。 类中的常量,是编译器记住常量值,而不需要实际的开销。 类中还可以定义类型,这个是元编程的重要支持。 我们来看一个元编程的if else。 在C++系统头文件中,有一个名为type_traits的文件里面有一个模板:conditional 前置定义是:
template<bool, typename, typename> struct conditional; 根据说明,conditional模板是根据第1个参数bool的值(true或false),来确定conditional中的type的类型。 如果bool 为 true,则type为第2个参数的类型,如果为false,则type为第3个参数的类型。 我们先说使用,比如,我们需要根据一个bool常量来定义一个数组,如果常量值为true,我们定义这个数组为uint8_t,否则定义为uint16_t: typename std::conditional<COND, uint8_t, uint16_t>::type array[20]; 这个就是定义array数组。cond为真,则是uint8_t array[20]。 实际使用远比这个复杂,太简单的情况也用不着mixin crtp等等。 这里要理解conditional的type,是怎么跟随bool参数变化的。 template<bool _Cond, typename _Iftrue, typename _Iffalse>
struct conditional
{ typedef _Iftrue type; };
// Partial specialization for false.
template<typename _Iftrue, typename _Iffalse>
struct conditional<false, _Iftrue, _Iffalse>
{ typedef _Iffalse type; }; 就这么几行。这里定义了“两个”conditional类模板。 第1个是普通的 template<bool _Cond, typename _Iftrue, typename _Iffalse> struct conditional { typedef _Iftrue type; }; 第1个在struct里直接定义了 type是_Iftrue。如果没有第2个conditional,那么我们永远只能得到一个固定的类型。 第2个conditional类模板定义,术语称为partial specialization,一般翻译为“偏特化”,就是部分特别处理。 // Partial specialization for false. template<typename _Iftrue, typename _Iffalse> struct conditional<false, _Iftrue, _Iffalse> { typedef _Iffalse type; }; 这个偏特化的类模板,固定了第1个参数bool为false,其余两个参数不变。 编译器就知道,如果程序里使用了conditional类模板,并且第1个参数为false的,就去匹配那个偏特化的版本。在这个版本的struct 里,定义了 type 类型为 _Iffalse,就是第3个参数的类型。 回到最开始的话题。C++可以根据一些条件(可以说就是类模板参数),来定义某类型。这离不开偏特化或全特化。 实际上,你看到的不同的类型,是分别属于不同特化版本的类模板。然而,对程序的使用来说,可以认为是透明的。就比如conditional类模板,如果你不去看type_traits文件,那么你可能根本不知道它具体是怎么实现的,你可能认为它就是“一个”类模板。 元编程就是通过提供了各种特化的类模板,每个模板中都有一个结果,来让程序不断地匹配,从而得到想要的结果。就是让编译器自己去选择合适的模板。这就是编译时计算。 你必须自己提供所有的结果,和达成这些结果的条件,编译器就会根据条件找到合适的结果。但是,程序中遇到数据,不只是常量。对于变量,模板不是很好用,需要一些辅助手段,把变量变换为常量,才能交给模板计算。 void foo(bool b) // b是输入的变量 { if (b) // 通过实际的语句判断变量值 // b 确定为 true,下面直接使用常量的true计算 conditional<true, ......... else // b 确定为 false, 下面直接使用常量的false计算
conditional<false, ....... } 类模板,通过给出的不同的模板参数,实例化成不同的类。元编程对变量不是很适用。处理常量非常好。但没有要求你在程序中只用元编程啊, 程序中可以把元编程和普通编程混合在一起。对常量就用元编程,对变量就用普通编程。一个程序如果全部是元编程的话,那么这个程序里没有变量。这种在实际应用中,基本没有。 元编程主要是为了解决程序中的常量运算,包括生成各种常量数据结构,也是非常合适的。最能说明问题的,是USB的配置描述符。我看目前好像都是人工在写描述符数据,使用元编程就可以自动生成。用户只要在类模板实例化时,提供一些“必要”的参数,像版本号,VID,PID等等。而描述符中的其它可计算数据,则完全可以由元编程来完成。比如描述符长度,个数等等,还有描述符中的各个ID。 其实,在描述符中,“必须”要用户给出的数据不多。而那些长度,id,interface从属关系,interface类别数据,endpoint 属性和方向等等,却比较繁杂,人工计算很容易出错。 看看这个例子,里面有多少描述符的影子: 在这里,你可以看到定义描述符所必须要用户给出的数据。这些数据是跑不掉的。 除此之外的其它数据,都通过元编程计算得出,并和用户给出的必要数据组合成了描述符。 描述符数据保存在类模板里,所以Get String Descriptor请求,都不必用户处理了,内部直接操作报告给host: Get Descriptor( Device, Configuration, String) 虽然简洁,但对于不习惯元编程的人来说,看起来确实有些晕。 要把用户逻辑尽量减少,在框架内包含尽量多的逻辑。但实例化后,还要尽量把必须的逻辑包含进程序,用不到的逻辑,尽量排除,还要保持最大的灵活性。 这个框架的灵活之处在于:
这里可以加入多个任意的 interface。这个的 interface 是 hid,我可以把 msc 的定义 interface 和 audio 定义的interface 都加在这里,这个 usb 设备就是一个复合设备了,同时具有 hid, msc, audio 功能。这个才是 mixin 的真正体现。 |