C++ 杂谈(6)

template template parameter

曾经不记得什么 case 想传递 std::vector or std::list,但是却不知道怎么写,因为 std::vector 并不是一个类,typename 传递的是某个类型,实际上 C++ 的模板机制支持传递 template template parameter,这个名字表示是一个 template 的 parameter,其语法是在 <> 里面使用 template <Param…> class TemplateParameter,这里 template 里面必须是给定某个实例的 TemplateParameter 的全部,我们常使用的 std::vector 等模板其实使用了 default parameter,虽然只写了一个参数,但是定义里面有两个或更多个模板参数,这时 Param… 必须有多个。注意 template 后的 Param… 一般如果不需要使用可以不写名字(只做 placeholder 用),往往具体的类型通过其他的参数传递。

这个功能很少用到,一般类似 std::vector 都有 subtype 可以直接获得参数的类型。可能更有用的是作为 policy class。更多讨论参考这里

头文件组织

在 C 的世界里面头文件 .h 和 .c 文件分工很明确:函数的声明、类型的定义(不存在实现部分)都在 .h 里面,实现在 .c 文件里面。在 C++ 的世界里,这个问题因为 template 的存在会变的复杂起来。

  • inclusion model,这基本是延续 C 的风格,但是对于模板类来说,将实现部分放在 .c 文件是一个很不好的做法,一般这样编译后除非有模板的特化否则不会被编译出任何 object code,将实现和声明放在一个文件自然是可以的,但是也存在一个问题:如果只是需要引用这个模板类(不需要实现部分),由于实现部分需要的头文件比声明更多导致也会 include 那些不必要的头文件,这当然就会降低编译速度;所以分离声明和实现到两个文件(实现的文件 include 声明)有一定的好处,但是坏处是引用的人需要知道什么时候需要什么,在 .c 文件里应该仅仅提供模板的特化:这样或者需要整个模板的人直接 include 定义头文件,或者使用特化情况的人可以仅仅 include 声明头文件,而在连接时和 .c 产生的 object 连接。
  • separation model,这是 C++ “关键字” export 引入的原因,可惜的是常见的 C++ 编译器对此都没有足够的支持,在这种模式下 template 可以被 export 修饰(export 只能修饰 template),然后就可以把实现分离出去。

最后提一下一个相关的问题。所谓的 forward declaration,为了进一步减少对其他头文件的依赖,我们可以选择 forward declaration,这在 effective C++ 里面甚至有所推崇,认为每个类都应该提供 foo.h、foo_fwd.h 和 foo.cpp,其实按照前面的建议模板类还有 foo_def.h。但是其实引入更多的文件,到底什么时候用哪个却缺少明确的规则:

  • 一般非模板类或者模板类,作为函数返回类型、输入类型,在当前 declaration 文件里可以用 forward declaration,如果是成员就得用 declaration,这是因为成员影响被定义的类对象的大小;在实现文件里需要使用细节(使用相关的方法),因此至少需要 declaration 文件
  • 对模板类来说其 _def 需要知道细节的就得使用 _def 或者非模板类 declaration 头文件

这个规律有待实践。某些情况下(互引)需要 forward declaration 解决。我们看一个 lua binding 里面一个关系,为什么这三种必须存在且需要一定的分离。我们前面解释 inheritance 用的例子 wrapper<T>、thread_ops<T> 和 thread/vm 的关系,其中 thread/vm 都可以创建新的 thread,所以 thread_ops 应该提供一个 new_thread 方法,它返回 thread 对象,因此 thread_ops 之前编译器必须知道 thread 是一个类,这时应该使用 thread 的 forward declaration,如果在 thread_ops 里面希望提供这个方法的实现,那意味着除了 forward declaration 是不够的,还需要其 declaration,因此应该将其 definition 从 declaration 中剥离,这样如果分文件的话 thread_ops.h 包含 thread_fwd.h,thread.h 包含 thread_ops.h,thread_ops_def.h 包含 thread.h =.=

更多讨论参看这里

precompiled header

把公用部分首先用 preprocessor 处理成一个文件,所有的编译单元直接 include 那一个文件。某种程度上可以加快(预)编译速度。

调试模板的技巧

  • 做好 shallow checking,特别现在有 type_traits 和 static_assert 了;
  • 使用 tracer 调试模板类、函数,tracer 是满足 template parameter 的 requirement 的 minimum 类,通过在其相关方法里面做一些额外的动作,我们就能将其传递给被测试的模板类、函数,并根据额外动作的结果分析
  • tracer 如果外接一个 assertion engine 我们一般称为 oracle 也是一种常见的测试技术
  • archetype 也是一种特殊的 tracer,除了 minimum requirement 以外它什么都不干,纯粹为了验证模板类的 minimum requirement 的

更多的 exception

  • class C 和 int C 可以共存,但是 template <typename T> class C 就不能
  • nested template 不仅仅方法,还有对应的类等等需要 .、-> 或者 :: 关系的都需要 template 表示
  • 注意需要 <::X> 的时候应该在 < 和 :: 之间加空格(类似 >> 那个 bug,不晓得 C++11 修复了这个没)
  • dependent base class 不会查找不完整 nondependent name,所以模板子类里面调用父类的方法必须使用 this-> 或者父类名:: 的形式,将其转换成为 dependent name,而后者在模板类实例化的时候会进行查找

demangling

记得 GCC 的 typeid 返回的类名都是 mangle 过的,可以使用 gcc 自己的 demangle utility 将其名字弄成人类可以看懂的,居然只有 C 的 API(见此 doc讨论)。

——————
Let my lord, I pray thee, pass over before his servant:and I will lead on softly, according as the cattle that goeth before me and the children be able to endure, until I come unto my lord unto Seir

C++ 杂谈(6)