类型萃取(Type Traits)

在C++中我们知道有bool、char、int、int * 等原生类型以及原生指针类型,同时还有自定义的 union、struct、class等类型。
现在有一个需求,给定任意一个类型,如果它是是一个数值类型,打印YES, 否则,打印NO。
比如,bool 可以看做数值类型的 0或1,char、int 本身就是数值类型,指针类型则不能看做是数值类型;自定义的类也不是数值类型。如果要实现这个需求,你会怎么做呢?

方案一:类型比较

一个比较直白的方案是使用 typeid(myint).name() 获得类型名,然后通过对比类型字符串来确定哪些变量是数值类型。代码如下:

template <typename T>
void printInteger(T x)
{
    const char * typeName = typeid(x).name();
    bool isInteger = false;
    if (strcmp(typeName, "int"))
    {
        isInteger = true;
    }
    else if (strcmp(typeName, "bool"))
    {
        isInteger = true;
    }
    else if (strcmp(typeName, "char"))
    {
        isInteger = true;
    }
    // 依次列举其他的类型 ...


    if (isInteger)
    {
        std::cout << "YES" << std::endl;
    }
    else
    {
        std::cout << "NO" << std::endl;
    }
}

面对这个简单的需求完全可以这么做。但是,这个方案需要在运行时判断类型,实际上这些类型都是在编译器就确定了的,为什么非要在运行时判断呢?

方案二:特性萃取

下面我给出一种利用了 C++ 模板特化特性实现的方案,可以在编译器就能够判断是否是数值类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
使用对象类型来模拟 bool,需要使用两个不同的类型,
这样就可以利用函数的参数类型推断特性了。
*/
struct Traits_TrueType { };
struct Traits_FalseType { };

/*
IsIntegerType 是一个类型,可以用来声明变量。
*/
template <typename T>
struct Traits_IsInteger
{
    typedef Traits_FalseType IsIntegerType;
};

template <>
struct Traits_IsInteger<int>
{
    typedef Traits_TrueType IsIntegerType;
};

template <>
struct Traits_IsInteger<bool>
{
    typedef Traits_TrueType IsIntegerType;
};


template <typename T1>
void printInteger(T1 x)
{
    // 声明 IsIntegerType 类型的变量
    typename Traits_IsInteger<T1>::IsIntegerType v;
    printInteger_imp(x, v);
}

/*
利用参数类型推导机制,分流到不同的函数中。
*/
template<typename T1>
void printInteger_imp(T1 x,  Traits_FalseType /* b */)
{
    std::cout << "NO" << std::endl;
}

template<typename T1>
void printInteger_imp(T1 x,  Traits_TrueType /* b */)
{
    std::cout << "YES" << std::endl;
}

使用方式同第一种方案一样:

    printInteger(int(1));
    printInteger(bool(true));

    struct A {};
    A a;
    printInteger(a);

可以看到第二种方案有点复杂,主要是利用了模板特化技术和参数类型推导机制。
首先,我们使用了两个 struct 类型来代表 "True" 和 "False"。这里的 Traits_TrueType 和 Traits_FalseType 是一个类型,可以用来定义变量。这样就达到了 "True" 和 "False" 是两种类型。而普通的 true 和 false 是 bool 类型的两个取值,属于同一个类型,无法用于函数参数的类型推导机制。
Traits_IsInteger 是一个 struct 模板类型,其内部只是定义了一个类型 IsIntegerType,并默认为 Traits_FalseType。在后面针对 int 、char、bool 等数值类型的模板特化中,定义为 Traits_TrueType,这样就区分出了哪些类型是数值类型,哪些不是。
最后,利用

     typename Traits_IsInteger<T1>::IsIntegerType v;

定义一个变量 v,它要么是 Traits_TrueType类型,要么是 Traits_FalseType。其实这个变量 v 的值并没有用处,它的类型才是我们需要的。不同的类型使得可以调用不同的函数重载实现,这就巧妙的达到了我们的目的。

总结

特性萃取技术是C++模板中的一个重要技法,在STL等泛型编程中广泛使用。可以这样说,如果你不懂特性萃取,基本上就告别了C++泛型编程了。
其实特性萃取技术给我最多的震撼还是在思路上的,哇哦,模板还可以这样用啊。