C++模板惯用法

时间:2023-01-31 21:43:15

关于 C++ 模板编程的惯用法,note-to-self + keynote + idiom case + cross-reference 式笔记

目录


模板语法^

称谓:函数模板 (function template) vs. 模板函数 (template function),或 类模板 (class template) vs. 模板类 (template class),见 [CPP TEMP] 7.1

两阶段编译 (two-phase lookup)、第一实例化点 (first point of instantiation),见 [CPP LANG] 13.2.5 [CPP TEMP] 2.1.2

实例化 (instantiation) 和特化(又译专门化)(specialization)、生成的特化 (generated specialization) vs. 显式的特化 (explicit specialization),见 [CPP LANG] 13.2.2, C.13.7 [CPP TEMP] 7.2

不完全实例化 (incomplete instantiation),见 [CPP LANG] 13.2.2 [MODERN CPP] 1.8

完全特化 (complete specialization)、部分特化(又译偏特化)(partial specialization),见 [CPP LANG] 13.5 [CPP TEMP] 3.3, 3.4

特化顺序:更特化 (more specialized)、更泛化 (more general)、原始模板 (primary template),见 [CPP LANG] 13.5.1 [CPP TEMP] 7.2

非类型模板参数,见 [CPP LANG] 13.2.3 [CPP TEMP] 4

函数模板和普通函数间的重载,见 [CPP LANG] 13.3.2 [CPP TEMP] 2.4

函数模板的参数推导 (argument deduction),见 [CPP LANG] 13.3.1, C.13.4 [CPP TEMP] 2.2, 11

默认模板参数,见 [CPP LANG] 13.4.1 [CPP TEMP] 3.5

成员模板 (member template),见 [CPP LANG] 13.6.2 [CPP TEMP] 5.3

模板作为模板参数 (template template parameter),见 [CPP LANG] C.13.3 [CPP TEMP] 5.4

typename 限定词 (typename qualifier),见 [CPP LANG] C.13.5 [CPP TEMP] 5.1 [EFFECT CPP] Item 42

template 限定词 (template qualifier),见 [CPP LANG] C.13.6 [CPP TEMP] 5.1

模板代码组织:包含模型 (inclusion model) vs. 分离模型 (separation model) 又称分别编译 (separate compile),见 [CPP LANG] 13.7 [CPP TEMP] 6.1, 6.3

显式实例化 (explicit instantiation),见 [CPP LANG] C.13.10 [CPP TEMP] 6.2

设计思维:运行时多态 (run-time polymorphism) vs. 编译时多态 (compile-time polymorphism) 又称参数化多态 (parametric polymorphism),见 [CPP LANG] 13.6.1 [EFFECT CPP] Item 41 [CPP TEMP] 14

模板惯用法示例^

堆栈上分配^

on-stack allocation 的 std 案例:tr1::array 模板

使用模板方法的不足之处是使用编译时确定的 buffer size,为了能在运行时动态调整 stack 内存分配数量,可借助 VC CRT 的 _alloca, _malloca 函数

示例:一个 printf 式的生成 std::string 的函数

[cpp] view plaincopy
  1. template <size_t BufSize, class CharT>  
  2. inline  
  3. std::basic_string<CharT> make_string(const CharT* format, ...)  
  4. {  
  5.     CharT buf[BufSize];  
  6.     va_list args;  
  7.   
  8.     va_start(args, format);  
  9.     // vsprintf 是函数模板, 其针对 char 特化调用 vsprintf_s, 针对 wchar_t 特化调用 vswprintf_s  
  10.     vsprintf(buf, BufSize, format, args);  
  11.     va_end(args);  
  12.   
  13.     // 注意: 返回时构造可让 VC 编译优化为只有一次 string ctor 调用, 没有额外 copy  
  14.     return std::basic_string<CharT>(buf);  
  15. }  

编译优化的开关^

bool 模板参数,或整数模板参数 + 阈值,避免重复代码时借助编译优化

示例:一个支持透明色的 32bit blit 函数

[cpp] view plaincopy
  1. template <bool UseMask>  
  2. void blit(int* dst, const int* src, int mask, size_t size)  
  3. {  
  4.     for (size_t i = 0; i < size; i++, dst++, src++) {  
  5.         if (!UseMask || *src != mask)  
  6.             *dst = *src;  
  7.     }  
  8. }  

推导数组元素个数^

可由参数推导求出数组的元素个数,要求必须是数组名,而非指向数组的指针或 new[] 数组

示例:VC CRT 的 _countof 计算数组的元素个数

[cpp] view plaincopy
  1. // 以 C++ 方式编译时, _countof 的定义如下  
  2. template <typename _CountofType, size_t _SizeOfArray>  
  3. char (*__countof_helper(UNALIGNED _CountofType (&_Array)[_SizeOfArray]))[_SizeOfArray];  
  4. #define _countof(_Array) (sizeof(*__countof_helper(_Array)) + 0)  

示例:多数 VC CRT buffer 操作函数都有 Secure Template Overloads 版本

[cpp] view plaincopy
  1. template <size_t size>  
  2. errno_t strcpy_s(char (&strDestination)[size], const char *strSource);  

推导常数^

示例:掩码常数 Mask<N>::Value

[cpp] view plaincopy
  1. #define _MASK_VAL(x)    (1 << x)  
  2. // 用于代替上面的宏  
  3. // 实例化超出 [0:31] 范围的 Mask 时, 产生编译警告 warning C4293  
  4. template <unsigned int N>  
  5. struct Mask {  
  6.     enum { Value = (1 << N) };  
  7. };  

示例:GCD<N, M>::Value 求解最大公约数

[cpp] view plaincopy
  1. // GCD<N, M> 原始模板  
  2. template <unsigned int N, unsigned int M>  
  3. struct GCD {  
  4.     static const unsigned int Value = GCD<M, N % M>::Value;  
  5. };  
  6.   
  7. // GCD<N, 0> 特化, 用以终止递归条件  
  8. template <unsigned int N>  
  9. struct GCD<N, 0> {  
  10.     static const unsigned int Value = N;  
  11. };  

隐式转换的显式函数 implicit_cast^

见 [CPP LANG] 13.3.1 模板函数参数推导

  1. 因为是 return u,而不是 return (T) u,所以是隐式转换
  2. 可推导的参数放到 template 参数列表的最后
  3. 效率:有两次拷贝(参数、返回值),但通常编译优化可将其减小到一次拷贝
[cpp] view plaincopy
  1. template <class T, class U> T implicit_cast(U u) { return u; }  
  2.   
  3. void func(int i)  
  4. {  
  5.     implicit_cast<double>(i);       // T 显式指定为 double, U 由参数推导得出 int  
  6.     implicit_cast<chardouble>(i); // i 先转换为 double, 再隐式转换为 char  
  7.     implicit_cast<char*>(i);        // 错误: int 不能隐式转换为 char*  
  8. }  

推导 callable 可调用物^

  1. 基于函数指针类型

    • 可提取 callable 的参数和返回值类型
    • callable 只接受函数指针
    [cpp] view plaincopy
    1. template <class RetT, class ArgT>  
    2. bool calc_and_check_1(RetT (*calc)(ArgT), ArgT arg, bool (*check)(RetT))  
    3. {  
    4.     const RetT& ret = calc(arg);  
    5.     return check(ret);  
    6. }  
  2. 基于直接的类型

    • callable 接受函数指针、函数对象、成员函数
    • std 算法使用这种方法

    提取 callable 的参数和返回值类型时,callable 只能是函数对象:

    • 函数对象类:含 typedef 指明参数和返回值类型,std 约定命名为 [first_|second_]argument_type, result_type,可从 binary_function, unary_function 继承
    • 函数指针:用 std::ptr_fun 转换为函数对象
    • 成员函数:用 std::mem_fun, tr1::bind 转换为函数对象
    [cpp] view plaincopy
    1. template <class Calc, class ArgT, class Check>  
    2. bool calc_and_check_2(Calc calc, ArgT arg, Check check)  
    3. {  
    4.     const Calc::result_type& ret = calc(arg);  
    5.     return check(ret);  
    6. }  
  3. 使用 tr1::function 或 boost::function

    • 和第 2 种方法类似,但是 callable 的参数和返回值类型是固定的
    • tr1::function 已放入 std 名字空间
    [cpp] view plaincopy
    1. bool calc_and_check_3(std::function<int (double)> calc, double arg, std::function<bool (int)> check)  
    2. {  
    3.     const std::function<int (double)>::result_type& ret = calc(arg);  
    4.     return check(ret);  
    5. }  

用成员模板实现继承隐喻^

见 [CPP LANG] 13.6.3.1

对于 Derived 和 Base class,并不隐喻 T<Derived> 和 T<Base> 之间存在内建的继承关系。更一般的:设 TypeA 到 TypeB 有隐式转换,并不隐喻 T<TypeA> 到 T<TypeB> 存在内建的隐式转换

可用成员模板实现 subclass 转换隐喻。通常隐式转换以这些成员函数表现:copy ctor, assign, operator Type,即实现这些成员函数的模板版本

示例:典型的 std::auto_ptr 实现,auto_ptr<Derived> 可拷贝或赋值给 auto_ptr<Base>

[cpp] view plaincopy
  1. template <class Type>  
  2. class auto_ptr {  
  3.     // 非模板的 copy ctor, assign 无法从下面的模板版生成, 需要显式写出, 否则将使用编译器生成的  
  4.     auto_ptr(auto_ptr& right) throw() : ptr_(right.release()) {}  
  5.   
  6.     // 模板的 copy ctor: 当  T2* => Type* 时, 隐喻 auto_ptr<T2> => auto_ptr<Type>  
  7.     // assign 和 operator auto_ptr<T2> 的模板版相似从略  
  8.     template <class T2>  
  9.     auto_ptr(auto_ptr<T2>& right) throw() : ptr_(right.release()) {}  
  10.   
  11.     Type* release() throw() {  
  12.         Type* temp = ptr_;  
  13.         ptr_ = 0;  
  14.         return temp;  
  15.     }  
  16.   
  17. private:  
  18.     Type*   ptr_;  
  19. };  

假设模板基类中的成员^

模板会小小的违背继承规则:Derived<T> 不能访问 Base<T> 的成员,除非告诉编译器假设它存在,即推迟检查到第二阶段编译,见 [EFFECT CPP] Item 43 [CPP TEMP] 9.4.2

假设 Base<T> 的成员 member 存在的方法有:

  1. 用 this->member
  2. 用 using Base<T>::member 导入名字
  3. 用 Base<T>::member,副作用是关闭 virtual member 的动态绑定

CRTP 循环模板模式^

见 [CPP TEMP] 16.3 CRTP: Curiously Recurring Template Pattern

示例:使用类专属的 set_new_handler 和 operator new,见 [EFFECT CPP] Item 49

[cpp] view plaincopy
  1. template <class T>  
  2. class NewHandlerSupport;    // 含针对类型 T 的 set_new_handler 和 operator new  
  3.   
  4. class Widget : public NewHandlerSupport<Widget> {};  

结合使用函数模板和类模板^

  1. 类模板能以函数对象类的形式保存调用环境,并有模板默认参数,但不能推导参数
  2. 函数模板能推导参数,但不能保存环境和有模板默认参数

两者结合后,用来写生成函数对象的工厂函数

std 案例:用于构造谓词的 binder, adapter 和 negater,见 [CPP LANG] 18.4.4

特化的基本目的^

  1. 解决对于特定类型实例化时的语义错误

    示例:数组字符串的比较

    [cpp] view plaincopy
    1. // 原始模板使用值比较  
    2. template <class T>  
    3. bool less(T l, T r)  
    4. {  
    5.     return l < r;  
    6. }  
    7.   
    8. // const char* 应该用 strcmp 比较  
    9. template <>  
    10. bool less(const char* l, const char* r)  
    11. {  
    12.     return strcmp(l, r) < 0;  
    13. }  
  2. 为特定类型实例化提供更高效的实现

    示例:交换 STL 容器 string, vector 等

    [cpp] view plaincopy
    1. // 原始模板使用值交换  
    2. template <class T>  
    3. void swap(T& l, T& r)  
    4. {  
    5.     T t(l);  
    6.     l = r;  
    7.     r = t;  
    8. }  
    9.   
    10. // std::string 最好用 swap 成员函数, 以发挥 pimpl 方式实现(假设)的效能  
    11. template <class CharT>  
    12. void swap(std::basic_string<CharT>& l, std::basic_string<CharT>& r)  
    13. {  
    14.     l.swap(r);  
    15. }  
  3. 主要依靠特化工作,反而原始模板的意义为次,traits 通常使用这种方法

    示例:编译时 assert 断言

    [cpp] view plaincopy
    1. // 静态/编译时 assert 断言, 要求 expr 能在编译时求值  
    2. // 如果 expr = false, 产生编译错误 error C2027  
    3. template <bool expr> struct StaticAssert;  
    4. template <> struct StaticAssert<true> {};  
    5. template <size_t size> struct StaticAssertTest {};  
    6.   
    7. #define STATIC_ASSERT(x)    \  
    8.     typedef StaticAssertTest<sizeof(StaticAssert<bool(x)>)>     StaticAssertType##__LINE__  

解决实例化的代码膨胀^

用提取共性的方法解决实例化产生的目标代码膨胀 (code bloat),见 [EFFECT CPP] Item 44

示例:部分特化转接调用完全特化解决代码膨胀,见 [CPP LANG] 13.5

[cpp] view plaincopy
  1. // 原始模板  
  2. template <class T>  
  3. class Vector;           // 含最一般的 [] 操作  
  4.   
  5. // Vector<void*> 是针对 void* 的完全特化  
  6. template <>  
  7. class Vector<void*>;    // 含针对 void* 的 [] 操作  
  8.   
  9. // Vector<T*> 是针对任意类型 T 指针的部分特化  
  10. template <class T>  
  11. class Vector<T*> : private Vector<void*> {  
  12.     typedef Vector<void*> Base;  
  13.   
  14.     // 转接调用完全特化 Vector<void*> 的 [] 操作  
  15.     T*& operator[](int i) {  
  16.         return reinterpret_cast<T*&>(Base::operator[](i));  
  17.     }  
  18. };  
  19.   
  20. // 部分特化 Vector<T*> 的实例化  
  21. // 代码空间开销: 所有的 Vector<T*> 实际共享一个 Vector<void*> 实现  
  22. Vector<Shape*>  vps;    // <T*> 是 <Shape*>, T 是 Shape  
  23. Vector<int**>   vppi;   // <T*> 是 <int**>, T 是 int*  

traits 特征和 policy 策略^

见 [CPP TEMP] 15 [CPP LANG] 13.4 [MODERN CPP] almost all book

traits 和 policy 使用相同的下层技术,只是在设计目的和作用上不同

  1. traits 的作用倾向于:特征提取,特征主要指类型和标识,以含 typedef, enum, static 著称,经常是空类

    traits 的 std 案例:std::iterator_traits,用法举例:根据 iterator 特征选择算法,如只对 random_access_iterator_tag 使用快速排序

  2. policy 的作用倾向于:行为组合,是编译时 Strategy 模式,以含行为正交的 member function 著称,经常多个 policy 组合而用

    policy 的 std 案例:std::allocator,用法举例:实现自己的 small-block allocator 让 STL 容器针对小块内存分配优化

示例:traits 类型标识

Q: 为什么不用 C++ 内建的 RTTI: typeid()
A: 即便 MyTraits<Type> 的 Type 不是多态类型,typeid() 可在编译时求值,但 type_info::operator== 或 type_info::name() + strcmp() 却很难在编译时求值,导致条件分支没法优化掉,见 [EFFECT CPP] Item 47, 48

[cpp] view plaincopy
  1. struct MyTraitsBase {  
  2.     enum {  
  3.         TYPE_UNKNOWN,  
  4.         TYPE_A,  
  5.         TYPE_B  
  6.     };  
  7. };  
  8.   
  9. template <class Type>  
  10. struct MyTraits : public MyTraitsBase {  
  11.     static const int TYPE_ID = TYPE_UNKNOWN;  
  12. };  
  13.   
  14. template <>  
  15. struct MyTraits<TypeA> : public MyTraitsBase {  
  16.     static const int TYPE_ID = TYPE_A;  
  17. };  
  18.   
  19. template <>  
  20. struct MyTraits<TypeB> : public MyTraitsBase {  
  21.     static const int TYPE_ID = TYPE_B;  
  22. };  
  23.   
  24. template <class Type>  
  25. void user(const Type& obj)  
  26. {  
  27.     // 实际中可用表驱动法替代判断语句  
  28.     // 开启编译优化后, 这里的条件分支被优化掉  
  29.     if (MyTraits<Type>::TYPE_ID == MyTraitsBase::TYPE_A)  
  30.         // 针对 TypeA 类型的处理  
  31.     else if (MyTraits<Type>::TYPE_ID == MyTraitsBase::TYPE_B)  
  32.         // 针对 TypeB 类型的处理  
  33.     else if (MyTraits<Type>::TYPE_ID == MyTraitsBase::TYPE_UNKNOWN)  
  34.         //  针对一般类型的处理  
  35. }  

参考书籍^

  • [CPP LANG] "C++ Programming Language, Special Ed" Ch13 Templates, Appendix C.13 Templates
  • [CPP TEMP] "C++ Templates: The Complete Guide", 2002
  • [EFFECT CPP] "Effective C++, 3Ed"
  • [MODERN CPP] "Modern C++ Design", 2001