C++ 可以通过多种特性来支持编译时编程,若所有必要的 输入都可用,编译器可以在编译时计算控制流的结果。
模板元编程
C++ 模板的一些特性可以与实例化过程结合,成为一种递归“编程语言”。
最简单的样例,在编译期计算是否为素数:
#include <iostream>
#include <string>
// 使用模板元编程,判断是否是素数
// 素数:大于1的自然数,且只有1和它本身能整除它
template<unsigned p, unsigned d>
struct DoIsPrime {
static constexpr bool value = (p%d != 0) && DoIsPrime<p, d-1>::value;
};
// 偏特化,也是递归终止条件
template<unsigned p>
struct DoIsPrime<p, 2> {
static constexpr bool value = (p%2 != 0);
};
template<unsigned p>
struct IsPrime {
static constexpr bool value = DoIsPrime<p, p/2>::value;
};
// 特化, 0 不是素数
template<>
struct IsPrime<0> {static constexpr bool value = false;};
// 特化, 1 不是素数
template<>
struct IsPrime<1> {static constexpr bool value = false;};
// 特化,2是素数
template<>
struct IsPrime<2> {static constexpr bool value = true;};
// 特化,3是素数
template<>
struct IsPrime<3> {static constexpr bool value = true;};
int main() {
std::cout << std::boolalpha << IsPrime<0>::value << std::endl;
std::cout << IsPrime<1>::value << std::endl;
std::cout << IsPrime<2>::value << std::endl;
std::cout << IsPrime<3>::value << std::endl;
std::cout << IsPrime<4>::value << std::endl;
std::cout << IsPrime<5>::value << std::endl;
std::cout << IsPrime<6>::value << std::endl;
std::cout << IsPrime<7>::value << std::endl;
std::cout << IsPrime<8>::value << std::endl;
std::cout << IsPrime<9>::value << std::endl;
}
使用 constexpr 进行计算
C++11 引入了一个新特性 constexpr,简化了各种形式的编译时计算,如果有输入适 当,可以在编译时对 constexpr 函数求值。
#include <iostream>
#include <string>
// 如果输入适当,可以在编译期就对 constexpr 函数求值
// 计算从2到d,是都不能整除 p
constexpr bool doIsPrime(unsigned p, unsigned d) {
return d != 2 ? ((p%d != 0) && doIsPrime(p, d-1))
: (p%2 != 0);
}
// 求素数,需要考虑边界情况
constexpr bool isPrime(unsigned p) {
return p < 4 ? (!(p<2))
: (doIsPrime(p,p/2));
}
int main() {
std::cout << std::boolalpha << isPrime(10) << std::endl; // 编译期计算
std::cout << isPrime(11) << std::endl; // 编译期计算
std::cout << isPrime(12) << std::endl; // 编译期计算
int n = 1000;
std::cout << isPrime(n) << std::endl; // n是变量,所以在运行时计算
constexpr int m = 2000;
std::cout << isPrime(m) << std::endl; // m 在编译期即可确定,所以其值也是编译期计算
}
在C++11标准之前, constexpr函数(即那些能够在编译时就被求值的函数)有严格的限制,其中之一就是函数体中只能有一个单一的return语句。在C++11和后续标准中,这些限制逐渐放宽,constexpr函数能够包含更多常规的控制结构,比如for循环和if条件语句,从而使得编译时计算的代码更为直观和简洁。
C++14 中,constexpr 函数可以使用通用 C++ 代码中的控制结构。
#include <iostream>
#include <string>
// C++14后,constexpr修饰的函数可以使用控制语句了
constexpr bool isPrime(unsigned p) {
for (unsigned int d=2; d<=p/2; ++d) { if (p % d == 0) {
return false; // found divisor without remainder
}
}
return p > 1;
}
int main() {
std::cout << std::boolalpha << isPrime(10) << std::endl; // 编译期计算
std::cout << isPrime(11) << std::endl; // 编译期计算
std::cout << isPrime(12) << std::endl; // 编译期计算
int n = 1000;
std::cout << isPrime(n) << std::endl; // n是变量,所以在运行时计算
constexpr int m = 2000;
std::cout << isPrime(m) << std::endl; // m 在编译期即可确定,所以其值也是编译期计算
}
编译器会尽可能地在编译时计算 constexpr 函数,但如果失败也不会阻止程序编译,而是在运行时给出正确答案。(除非是用作数组长度或者用作非类型模板参数等必须是常量的场景,这种情况下,如果编译期未推导出来则会报错)
使用偏特化的执行路径选择
在编译时可以使用偏特化在不同实现之间进行选择;
#include <iostream>
#include <string>
constexpr bool doIsPrime(unsigned p, unsigned d) {
return d != 2 ? ((p%d != 0) && doIsPrime(p, d-1))
: (p%2 != 0);
}
// 求素数,需要考虑边界情况
constexpr bool isPrime(unsigned p) {
return p < 4 ? (!(p<2))
: (doIsPrime(p,p/2));
}
// 模板类声明
template<int Size, bool = isPrime(Size)>
struct Helper;
// 特化,分支1,如果是素数,输出分支1
template<int Size>
struct Helper<Size, true> {
Helper(){
std::cout<<"Branch 1" << std::endl;
}
};
// 特化,分支2,如果不是素数,输出分支2
template<int Size>
struct Helper<Size, false> {
Helper(){
std::cout<<"Branch 2" << std::endl;
}
};
int main() {
constexpr int a = 5; // 5 是素数,输出分支1
constexpr int b = 6; // 6 不是素数,输出分支2
Helper<a> h1;
Helper<b> h2;
return 0;
}
在C++编程中,函数模板不具备像类模板那样可以直接进行偏特化的能力,即不能针对函数模板的某些特定模板参数值提供专门的、独立的实现版本。这是因为函数模板的重载是基于函数签名的整体匹配,而不是像类模板那样可以根据模板参数的不同值产生不同的类定义。
当需要根据模板参数的具体类型或值来改变函数的行为时,编译器并不能直接通过函数模板偏特化来实现。这时,开发者需要借助其他的编程技术来达到类似的效果。例如:
-
使用带有静态函数的类:创建一个带有静态成员函数的类模板,利用模板参数在静态函数内部实现不同的逻辑。
-
std::enable_if:结合类型特征,使用SFINAE(Substitution Failure Is Not An Error)原理,通过std::enable_if或其他类似手段有条件地启用或禁用函数模板的实例化。
-
SFINAE特性:利用模板替换失败时不会导致编译错误的特点,通过重载模板函数并在模板参数的替换过程中产生有意义或无意义的结果来选择合适的函数模板。
-
编译时 if:自C++17起,可以通过编译时if语句(if constexpr)在编译时期决定函数内的执行路径。
SFINAE(替换失败不为过)
SFINAE(Substitution Failure Is Not An Error,替换失败不为过)是C++模板元编程中的一个重要原则。这个原则体现在编译器处理模板参数推导和函数模板重载选择的过程中。当编译器在尝试匹配和实例化模板时,如果某个模板参数的替换导致了语法上的错误(如类型不兼容、成员不存在、类型不完整等),按照SFINAE原则,编译器并不会因此报错,而是把这个模板实例化候选当作不合适而排除掉,继续尝试其他可能的模板实例化。
举个例子,假设有一个函数模板,它尝试获取某种类型的迭代器:
template<typename Container>
typename Container::iterator begin(Container& c);
如果传入一个不是容器的类型(如一个内置数组),编译器在尝试推导Container::iterator
时会失败,因为内置数组没有iterator
成员。依据SFINAE原则,编译器会识别到这个失败并跳过这个模板,转而去寻找其他匹配的函数。
此外,SFINAE还可以配合类型特征如std::enable_if
来实现条件编译,使得函数模板只在满足特定条件的情况下才会被实例化。例如:
template<typename T>
typename std::enable_if<std::is_integral<T>::value>::type
process(T value);
在这个例子中,process
函数模板只会对整数类型T生效,如果不是整数类型,SFINAE会排除这个模板实例化,从而不会产生编译错误。
SFINAE允许模板作者设计一系列重载的模板函数或类模板,通过替换失败自动排除不适用的选项,从而让编译器能选择出最合适的模板实例。这个机制极大地增强了模板在设计泛型算法和接口时的灵活性和可控性。
编译时 if
偏特化、SFINAE 和 std::enable_if 允许启用或禁用模板。C++17 引入了编译时 if 语句,允许我 们根据编译时条件启用或禁用特定语句。使用 if constexpr(…) 语法,编译器使用编译时表达式来决 定是应用 then 部分,还是 else 部分 (如果有的话)。
一个经典的例子,打印参数包中的所有参数,在过去,需要对这个函数编写一个特化版本,来作为递归结束的条件,而现在有了 if constexpr(…) ,就可以更简单地实现该逻辑了:
#include <iostream>
#include <string>
template<typename FirstArg, typename... Args>
void print(FirstArg&& firstArg, Args&&... args)
{
std::cout << firstArg << std::endl;
// 由if constexpr决定是否递归下去
if constexpr( sizeof...(args) > 0) {
print(std::forward<Args>(args)...);
}
}
int main() {
print("hello", "world", 1, 2.9, 3, 4, 5, 6);
}
constexpr 可以用于任何函数,而不仅在模板中, 也就是说,在main函数中都可以使用。
C++17有了constexpr,执行路径选择,变得更加简单直观:
#include <iostream>
#include <string>
constexpr bool doIsPrime(unsigned p, unsigned d) {
return d != 2 ? ((p%d != 0) && doIsPrime(p, d-1))
: (p%2 != 0);
}
// 求素数,需要考虑边界情况
constexpr bool isPrime(unsigned p) {
return p < 4 ? (!(p<2))
: (doIsPrime(p,p/2));
}
// 在编译期已经决定好最终走哪条分支了
template<int Size>
void foo() {
if constexpr(isPrime(Size)) {
std::cout << "branch 1" << std::endl;
}
else {
std::cout << "branch 2" << std::endl;
}
}
int main() {
foo<5>(); // branch 1
foo<8>(); // branch 2
}
总结
• 模板提供了在编译时进行计算的能力 (使用递归进行迭代,使用偏特化或三元操作符进行选 择)。
• 使用 constexpr 函数,可以将大多数编译时计算替换为,可在编译时上下文中调用的“普通函 数”。
• 使用偏特化,可以根据特定的编译时约束,在类模板的不同实现之间进行选择。
• 模板只在需要的时候使用,在函数模板声明中的替换不会导致代码无效。这个原则称为
SFINAE(替换失败不为过)。
• SFINAE只能用于为特定类型和/或约束提供函数模板。
• C++17起,编译时if允许根据编译时条件(甚至在模板外部)启用或丢弃语句。