C++ 对多个类型模板特化那些事儿

By | 2016-11-12

前言

模板特化大家都很熟了

template<typename T>
class Holder;

template<>
class Holder<TypeA> {};

template<>
class Holder<TypeB> {};

不过不知各位是否遇到过这种尴尬情况

typedef int TypeA;
typedef int TypeB;

看着很傻, 但是在 C艹 世界里还是很常见的, 比如对不同平台适配各种 int 类型的时候,
虽说有 stdint, 但要适配现有代码的时候还是难以避开这些问题

于是引出了本文的主题: 如何对一堆未知是否相同的类型做模板特化


strong typedef 法

这玩意儿在 boost 里面也有, 叫 BOOST_STRONG_TYPEDEF

大致原理是生成个新类型做包装

class WrapperA
{
public:
    explicit WrapperA(const int) {...}
    WrapperA(WrapperA const &) {...}
    WrapperA(void) {...}
    WrapperA &operator = (const int) {...}
    WrapperA &operator = (WrapperA const &) {...}
    operator const int &() const {...}
    operator int &() {...}
    bool operator == (WrapperA const &) const {...}
    bool operator != (WrapperA const &) const {...}
    bool operator < (WrapperA const &) const {...}
    bool operator <= (WrapperA const &) const {...}
    bool operator > (WrapperA const &) const {...}
    bool operator >= (WrapperA const &) const {...}
private:
    int v;
};

// WrapperB 同理, 完全重新定义一份

不过这玩意儿也不少缺点:

  • 略微有点性能损失, 虽然大多数场景可以忽略
  • 对于模板特化来说, 实际上 WrapperA 和 int 还是不一样的类型,
    你得确保你的应用代码完全使用你的 wrapper 类型
  • 对于需要算术运算的基本类型来说, 还有更多的麻烦事:

    • 你得重载各种 operator 用来支持算数运算, 完整的大约得有 20 个左右,
      而且还有各种类型运算后是需要返回新类型的 (比如 long 与 float 相乘结果是 double)

    • 你很卖力的重写完了所有 operator, 不出意外还会碰到各种 ambiguous

    • 最终, 你发现使用 explicit 构造函数可以避免这种情况, 但是, 又发现以下最基本的初始化都不好用了:

      BOOST_STRONG_TYPEDEF(unsigned int, my_uint)
      my_uint n = 0;
      // 这里 0 默认是 int, 然而由于 explicit 构造函数, 无法隐式转换
      
      void func(my_uint n) {...}
      func(0); // 同理, 这边也无法隐式转换, 必须手动 func((my_uint)0);
    • 总之, 这玩意儿不管怎样都无法完美的对应用层透明

模板递归法

这玩意儿是最近脑洞出来的结果, 不一定是最好方案, but it works ╮(╯▽╰)╭


首先, 我们先来熟悉下不是很常见的模板递归:

template<int N>
class Holder
{
public:
    enum {value = Holder<N - 1>::value + 1};
};
template<>
class Holder<0>
{
public:
    enum {value = 0};
};

int result = Holder<2>::value;

上述 result 的结果是 2, 实际上, 引用 Holder<2> 时会因为 Holder<N - 1> 而依次递归生成
Holder<2> Holder<1> Holder<0>, 并最终停止于 Holder<0> 的特化版本

模板递归全部在编译期完成, 因此最终值本身可以作为编译期参数来参与更复杂的模板推导 (推倒什么的最讨厌了 @w@)


讲到这, 应该知道接下来的套路了吧? 用模板递归配合 int 类型模板参数特化, 来针对相同类型推导出不同模板

不过, 还剩个问题, int 类型的特化值哪来? 手动敲可不靠谱, 毕竟你并不知道到底在什么时候需要特化

于是, 还得引入个静态计数, 在编译期做一个自增的数值

这玩意儿有很多的实现方式, 有的编译器自己也有提供, 这里提供一个可以跨平台的版本 (出处找不着了, 求指正)

template<int N>
class _Counter
{
public:
    char data[N + 1];
};
template<typename T_Type, int N, int Acc>
_Counter<Acc> _CounterSeen(T_Type, _Counter<N>, _Counter<Acc>);
template<typename T_Type>
class _CounterWrap
{
public:
    static T_Type value(void);
};
#define CounterGet(...) \
    (sizeof _CounterSeen(_CounterWrap<__VA_ARGS__>::value(), _Counter<1>(), _Counter< \
    (sizeof _CounterSeen(_CounterWrap<__VA_ARGS__>::value(), _Counter<2>(), _Counter< \
    (sizeof _CounterSeen(_CounterWrap<__VA_ARGS__>::value(), _Counter<4>(), _Counter< \
    (sizeof _CounterSeen(_CounterWrap<__VA_ARGS__>::value(), _Counter<8>(), _Counter< \
    (sizeof _CounterSeen(_CounterWrap<__VA_ARGS__>::value(), _Counter<16>(), _Counter< 0 \
         >()).data - 1) \
         >()).data - 1) \
         >()).data - 1) \
         >()).data - 1) \
         >()).data - 1) // 需要更多的话, 这里可以继续添加
#define CounterInc(...) \
    _Counter<CounterGet(__VA_ARGS__) + 1> _CounterSeen(__VA_ARGS__, \
            _Counter<(CounterGet(__VA_ARGS__) + 1) & ~ CounterGet(__VA_ARGS__)>, \
            _Counter<(CounterGet(__VA_ARGS__) + 1) & CounterGet(__VA_ARGS__)> \
        );

// 用法
CounterInc(MyType)
char v0[CounterGet(MyType)]; // char [1]
CounterInc(MyType)
char v1[CounterGet(MyType)]; // char [2]

大约就是每次申明一个函数, 在这里面实例化了模板, 并最终导致 sizeof 计算的结果逐渐增大


好了, 万事俱备, 开始我们蛋疼的模板特化吧

// 原始版本
template<typename T, int T_Loop>
class Holder
{
public:
    static void func(void)
    {
        Holder<T, T_Loop + 1>::func();
    }
};

// 偏特化版本
template<typename T>
class Holder<T, 4>
{
public:
    static void func(void)
    {
        defaultAction();
    }
};

typedef int MyTypeA;
typedef int MyTypeB;

// 第一次特化
CounterInc(MyTypeA)
template<>
class Holder<MyTypeA, CounterGet(MyTypeA)>
{
public:
    static void func(void)
    {
        // 特化版本
        specializaionAction();
    }
};

// 再来一次特化, 注意这里 MyTypeB 其实和 MyTypeA 完全相同
CounterInc(MyTypeB)
template<>
class Holder<MyTypeB, CounterGet(MyTypeB)>
{
public:
    static void func(void)
    {
        // 特化版本
        specializaionAction();
    }
};

注意到其间的玄机了嘛

  • 原始版本采用模板递归调用下一个版本, 如果没有提供任何特化, 将最终停止于偏特化版本
  • 如果提供了特化, 由于 T_Loop 小于我们默认的偏特化版本, 所以优先被使用到
  • 如果对同类型提供了多次特化, 那么首次特化的版本将被使用, 后续的版本相当于永远不会被使用到

注意事项

虽然这方法看上去很美, 但是依旧还是有坑的:

  • 递归模板特化会严重消耗编译期的时间, 尤其模板如果需要对外的话, 是需要完全在头文件的, 代码污染性比较严重, 建议谨慎使用
  • Windows 平台, 该方法非常容易造成 C1001 (大约就是编译器内部堆栈爆了), 也可能搞出各种诡异问题

转载请注明来自: http://zsaber.com/blog/p/139

既然都来了, 有啥想法顺便留个言呗? (无奈小广告太多, 需审核, 见谅)

Category: C++

发表评论

您的电子邮箱地址不会被公开。