引言
社区(http://purecpp.org/)里有朋友提出了编译期分割字符串的需求,大家在群里进行了热烈的讨论,也做了许多尝试,但并没有得出确定的结果。本文作者试图对C++11/14里的新关键字constexpr进行编译期计算的总结和探讨,并在最后结合constexpr给出C++在编译期分割字符串的方法。
一、编译期计算
我们先来看一段求阶乘(factorial)的算法:
size_t factorial(size_t n) noexcept
{
return (n == 0) ? 1 : n * factorial(n - 1);
}
很明显,这是一段运行期算法。程序运行的时候,传递一个值,它可以是一个变量,也可以是一个常量:
int main(void)
{
std::cout << factorial(10) << std::endl;
return 0;
}
如果程序仅仅像这样传递常量,我们可能会希望能够让它完全在编译的时候就把结果计算出来,那么代码改成这样或许是个不错的选择:
template <size_t N>
struct factorial
{
enum : size_t { value = N * factorial<N - 1>::value };
};
template <>
struct factorial<0>
{
enum : size_t { value = 1 };
};
int main(void)
{
std::cout << factorial<10>::value << std::endl;
return 0;
}
只是用起来会稍显麻烦点,但好处是运行期没有任何时间代价。
像上面这种运用模板的做法,算是最简单的模板元编程了。对C++模板来说,类型和值是同一种东西;同时,又由于C++的模板有了“Pattern Matching”(即特化和偏特化),同时又允许模板的递归结构(见上面factorial中使用factorial的情况),于是C++的模板是图灵完全的一种独立于C++的语言。理论上来说,我们可以利用它在编译期完成所有计算——前提是这些计算的输入都是literal的。
二、C++11以后的新限定符:constexpr
从C++11开始,我们有了constexpr specifier。它可以被用于变量,及函数上,像这样:
template <size_t N>
struct t_factorial_
{
enum : size_t { value = N * t_factorial_<N - 1>::value };
};
template <>
struct t_factorial_<0>
{
enum : size_t { value = 1 };
};
template <size_t N>
constexpr auto t_factorial = t_factorial_<N>::value;
int main(void)
{
std::cout << t_factorial<10> << std::endl;
return 0;
}
当然了,上面更直接的用法是这样:
constexpr size_t c_factorial(size_t n) noexcept
{
return (n == 0) ? 1 : n * c_factorial(n - 1);
}
在C++11中,constexpr还有诸多限制,但到了C++14,它似乎有点过于强大了。比如我们可以在函数中写多行语句,定义变量,甚至是循环:
// runtime version
template <typename T, size_t N>
size_t r_count(T&& v, const T(&arr)[N]) noexcept
{
size_t r = 0;
for (const auto& a : arr) if (v == a) ++r;
return r;
}
// constexpr version
template <typename T, size_t N>
constexpr size_t c_count(T&& v, const T(&arr)[N]) noexcept
{
size_t r = 0;
for (const auto& a : arr) if (v == a) ++r;
return r;
}
就如同我们在写的只是一个普通函数,之后在函数的最前面加上constexpr它马上就可以在编译期执行了。
constexpr同样带来了强大的类型计算能力。我们简单的来看个例子,实现一个“types_insert”(Reference:C++的杂七杂八:使用模板元编程操作类型集合):
template <typename...>
struct types {};
template <typename T, typename... U>
constexpr auto insert(types<U...>) noexcept
{
return types<T, U...>{};
}
可以看到,对于这种简单的类型计算,constexpr比模板元的实现要清晰很多。
不过,由于函数模板缺少偏特化,因此需要编译期分支判断的“types_assign”是没办法直接写出来的(在这里无法短路求值的std::conditional并没有什么用)。要知道,函数重载虽然强大,但仅能做编译期类型,而不是数值的Pattern Matching,这点是不如类模板的偏特化/特化的。
不过我们可以利用类模板的偏特化来模拟函数模板的偏特化:
template <int N, typename T>
struct impl_
{
constexpr static auto assign(void) noexcept
{
return insert<T>(impl_<N - 1, T>::assign());
}
};
template <typename T>
struct impl_<0, T>
{
constexpr static auto assign(void) noexcept
{
return types<>{};
}
};
template <int N, typename T>
constexpr auto assign(void) noexcept
{
return impl_<N, T>::assign();
}
但是说实话,我并不喜欢这样,这种写法丧失了函数模板的简洁性。一般来说,大家也不会用constexpr做太复杂的类型计算,这里反而用模板元来做会更加清晰些。从上面可以看出来,在做类型计算的时候,return返回的数值并不是我们需要的,而类型结果一般会用decltype取出来。在这种情况下,不使用constexpr,仅用普通函数都是可以的。
真正让人眼前一亮的,应该还是上面c_count的写法。利用模板元做数值计算其实是它的短板。撰写复杂不说,还有不少的局限性。
比如c_count可以这样用:
template <size_t N>
struct Foo { enum : size_t { value = N }; };
int main(void)
{
std::cout << Foo<c_count(',', "1, 2, 3, 4, 5, 6, 7, 8, 9, 0")>::value << std::endl;
return 0;
}
而模板元对string literal这类数值做计算是比较麻烦的,template non-type arguments被限制为常整数(包括枚举),或指向外部链接对象的指针(严格来说不止这