引言

考虑这样的一个需求,有一个函数fun针对类型是否有拷贝构造函数可以有不同的实现方式,假如A有拷贝构造函数, B无拷贝构造函数, 有拷贝构造函数的实现性能更为高效:

class A {
public:
    A() : i(1) {}
    int i;
};

class B {
public:
    B() : i(1) {}
    B(const B&) = delete;
    int i;
};

template<typename T>
void fun(T& t) {
    std::cout << "fun for copy constructible\n";
    std::cout << "below is optimized implementation\n";

    // ...

}

template<typename T>
void fun(T& t) {
    std::cout << "fun for copy non-constructible\n";
    std::cout << "below is common implementation\n";

    // ...

}

如何让编译器知道根据是否有拷贝构造函数实例化相应的fun实现呢?

C++标准库提供的enable_if元函数(meta function)它能够根据某些条件是否满足(例如是否有拷贝构造)来决定声明并定义哪一个符号,统称条件定义。

enable_if的实现相当简单:

template<bool B, typename T = void>
struct enableIf {
    typedef T type;
};

// 偏特化,B为false的时候没有type类型成员
template<typename T>
struct enableIf<false, T> {};

如果eableIf的第一个参数为false就偏特化,此时没有type类型成员,利用这个特性,就可以实现条件定义:

template<typename T>
typename enableIf<std::is_copy_constructible<T>::value>::type fun(T& t) {
    std::cout << "fun for copy constructible\n";
    std::cout << "below is optimized implementation\n";

    // ...

}

template<typename T>
typename enableIf<!std::is_copy_constructible<T>::value>::type fun(T& t) {
    std::cout << "fun for copy non-constructible\n";
    std::cout << "below is common implementation\n";

    // ...

}

std::is_copy_constructible<T>也是一个元函数,顾名思义,它的value就是指明T类型是否有拷贝构造函数。上述实现中,如果enableIf第一个模板参数为true,有type类型成员,例如:

int main() {
    A a;
    fun(a);

    return 0;
}

因为 A 有拷贝构造函数,所以enableIf有type类型成员,编译器定义优化版本的fun函数。输出:

fun for copy constructible
below is optimized implementation

如果enableIf第一个模板参数为false,就没有type类型成员,例如:

int main() {
    B b;
    fun(b);

    return 0;
}

因为B没有有拷贝构造函数,所以enableIf没有有type类型成员,编译器定义普通fun函数。输出:

fun for copy non-constructible
below is common implementation

实际上,可以将typename enableIf<std::is_copy_constructible<T>::value>::type看成是一个类型函数`EnableIf:

template<bool B, typename T = void>
using EnableIf=typename enableIf<B, T>::type;

它的参数列表在<>中,可以是值或者类型,输出是一个类型。这样上述的fun函数可写为更直观的版本:

template<typename T>
EnableIf<std::is_copy_constructible<T>::value> fun(T& t) {
    std::cout << "fun for copy constructible\n";
    std::cout << "below is optimized implementation\n";

    // ...

}

template<typename T>
EnableIf<!std::is_copy_constructible<T>::value> fun(T& t) {
    std::cout << "fun for copy non-constructible\n";
    std::cout << "below is common implementation\n";

    // ...

}

到这里,你应该对元编程有一点直觉的认识了。利用模板编写一个类型函数,将它作为生成器,在编译时生成类型和函数,这就是元编程了。相比而言,泛型编程也使用模板,但它更强调的是编写通用的类或者函数,而元编程强调编译时计算(比如类型函数的计算)。

实际上,利用模板和实例化机制进行元编程可以做到任何其他语言能做的事。接下来你将看到元编程有控制结构,可以完成编译时迭代(以递归的形式),它是图灵完备的,可以把它看成是一门编译时函数式编程语言。

元编程的选择控制结构

Conditional 二选一

如果要在两个类型中进行选择,可以实现一个conditional类型函数,它就像?:运算符对两个值进行选择一样对两个类型进行选择。conditional实现也很简单:

template<bool C, typename T, typename F>
struct conditional {
    using type = T;
};

template<typename T, typename F>
struct conditional<false, T, F> {
    using type = F;
};

template<bool C, typename T, typename F>
using Conditional = typename conditional<C, T, F>::type;

例如,想根据类型Ty是否是多态类型,返回相应的类型XY来定义一个对象z,就可以这样做:

Conditional<(Is_polymorphic<Ty>()), X, Y> z;

如果为是多态,就:

X z;

不是多态,就:

Y z;

Select 多选一

类似Conditional,Select类型函数根据第一个非类型参数N返回第N个类型:

template<unsigned N, typename ... Cases>
struct select;

template<unsiged N, typename T, typename ... Cases>
struct select<N, T, Cases...> : select<N-1, Cases...> {
};

template<typename T, typename ... Cases>
struct select<0, T, Cases...> {
    using type = T;
}

template<unsigned N, typename ... Cases>
using Select = typename select<N, Cases...>::type;

上述实现用到了可变参数模板,和普通递归一样,有一个下界条件,当到达N = 0时,使用偏特化版本select返回type

Select类型函数一个实际用处是配合元祖std::tuple使用。

元编程的迭代和递归

在编译时不能使用变量,所以元编程一般使用递归实现编译时迭代。例如一个阶乘函数模板:

template<int N>
constexpr int fac() {
    return N * fac<N - 1>();
}

template<>
constexpr int fac<1>() {
    return 1;
}

constexpr int x5 = fac<5>();

可以看到,和函数式编程一样,元编程处理一系列值的方式是递归调用,知道终止条件。这里的终止条件就是一个特例化函数模板。

总结

可以看到,元编程可以在编译时做任何计算。但是应该注意,它具有代码易读性差、调试难读高等缺点。所以在使用元编程前要考虑把计算提前到编译时是否值得。如下场景是值得的:

  • 做安全检查。如Effective C++而言,把程序的错误尽量提前永远是值得的;
  • 提高类型安全。计算数据的确切类型,消除很多显示类型转换;
  • 提高运行时性能。在编译时选择运行时要调用的函数,或者计算运行时需要的数据。

参考文献