C++ 有很多方式可以实现编译时计算,而模板为编译时计算提供了更多的可能。C++ 有多个特性来支持编译时编程:
constexpr
特性中使用直观的执行路径选择和大多数的语句类型(C++14,包括 for 循环、switch 等)让编译时计算支持的越来越好。本文将介绍这些特性。
不同于动态语言在运行时泛化,模板是在编译时实例化。事实证明,C++ 模板的一些特性可以与实例化过程结合起来,在 C++ 语言本身中产生一种原始的递归 “编程语言” 。因此,模板可以用于计算程序的结果。例如,下面的代码用于计算给定的数是否为质数:
template<unsigned p, unsigned d> // p: number to check, d: current divisor
struct DoIsPrime {
static constexpr bool value = (p%d != 0) && DoIsPrime<p,d-1>::value;
};
template<unsigned p> // end recursion if divisor is 2
struct DoIsPrime<p,2> {
static constexpr bool value = (p%2 != 0);
};
template<unsigned p> // primary template
struct IsPrime {
// start recursion with divisor from p/2:
static constexpr bool value = DoIsPrime<p,p/2>::value;
};
// special cases (to avoid endless recursion with template instantiation):
template<>
struct IsPrime<0> { static constexpr bool value = false; };
template<>
struct IsPrime<1> { static constexpr bool value = false; };
template<>
struct IsPrime<2> { static constexpr bool value = true; };
template<>
struct IsPrime<3> { static constexpr bool value = true; };
例如对于下面的表达式
IsPrime<9>::value
被扩展为:
9%4!=0 && 9%3!=0 && 9%2!=0
结果为 false
。
这里给出模板元编程的简单应用,关于模板元编程在后续章节还有详细介绍。
C++11 引入的 constexpr
大大简化了多种形式的运行时计算。然而 C++11 的 constexpr
函数有严格的限制(例如 constexpr
函数被限制只能有一条 return
语句组成。),大多数的这些限制在 C++14 中被取消了。当然,能够在编译时成功计算的前提条件是所有的计算步骤在编译时可能的,否则只能在运行时才计算出结果。
例如,使用 C++11 的 constexpr
计算质数:
constexpr bool
doIsPrime (unsigned p, unsigned d) // p: number to check, d: current divisor
{
return d!=2 ? (p%d!=0) && doIsPrime(p,d-1) // check this and smaller divisors
: (p%2!=0); // end recursion if divisor is 2
}
constexpr bool isPrime (unsigned p)
{
return p < 4 ? !(p<2) // handle special cases
: doIsPrime(p,p/2); // start recursion with divisor from p/2
}
因为 C++11 的 constexpr
函数被限制只能有一条 return
语句,我们这里利用了问号表达式的方法实现。C++14 去除这个限制,实现起来会更加容易,下面是使用 C++14 的 constexpr
函数来实现:
constexpr bool isPrime (unsigned int p)
{
for (unsigned int d=2; d<=p/2; ++d) {
if (p % d == 0) {
return false; // found divisor without remainder
}
}
return p > 1; // no divisor without remainder found
}
例如 isPrime(9)
,在编译时即可计算出结果。而下面的例子只能在运行时计算:
int x;
...
std::cout << isPrime(x); // evaluated at run time
编译时编程的一个应用是使用模板偏特化来选择不同的模板实现。例如,我们可以根据模板参数是否为质数来选择不同的实现:
// primary helper template:
template<int SZ, bool = isPrime(SZ)>
struct Helper;
// implementation if SZ is not a prime number:
template<int SZ>
struct Helper<SZ, false>
{
...
};
// implementation if SZ is a prime number:
template<int SZ>
struct Helper<SZ, true>
{
...
};
template<typename T, std::size_t SZ>
long foo (std::array<T,SZ> const& coll)
{
Helper<SZ> h; // implementation depends on whether array has prime number as size
...
}
这里,根据 std::array<>
的 size 是否为质数,选择不同的 Helper
实现。
由于函数模板不支持偏特化,可以使用以下机制根据某些限制改变模板的实现:
std::enable_if
,在 C++ 中,根据不同的参数类型重载函数十分常见。编译器遇到一个重载函数的调用时,会根据实参的类型匹配最佳的实现。编译器在编译时决定函数调用时,如果重载中包含函数模板,就会进行模板特化,也即将模板函数的参数和返回值类型根据实参进行替换,如果替换失败,也即模板特化失败,直接忽略这次替换,不会导致编译错误,这就是 ”替换失败并非错误“ ,也即 SFINAE(Substitution Failure Is Not An Error)。
这里 给了一个很好的例子,利用 SFINAE 在编译时判断一个类型是否有iterator:
template <typename T>
struct has_iterator {
template <typename U>
static char test(typename U::iterator* x);
template <typename U>
static long test(U* x);
static const bool value = sizeof(test<T>(0)) == 1;
};
int main() {
has_iterator<vector<int> > test;
if( test.value )
cout << "vector have iterator" << endl;
else
cout << "vector not have iterator";
has_iterator<int> test2;
if( test2.value )
cout << "int have iterator" << endl;
else
cout << "int not have iterator" << endl;
}
另外像 C++ 中的 std::enable_if
、std::is_class
、std::void_t
以及 C++11 的 type traits
都是使用了 SFINAE 特性,例如 std::is_class 的实现:
template<typename T>
class is_class {
typedef char yes[1];
typedef char no [2];
template<typename C> static yes& test(int C::*); // selected if C is a class type
template<typename C> static no& test(...); // selected otherwise
public:
static bool const value = sizeof(test<T>(0)) == sizeof(yes);
};
再看下面这个例子:
// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{
return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{
return t.size();
}
这里定义了两个函数模板:
T(&)[N]
,代表一个有 N
个 T
类型的数组。T
,这没有什么限制要求,但是限制了返回值类型为 T::size_type
,这就需要参数类型有成员 size_type
。当传递一个原始数组或者字符串字面值时,匹配第一个函数模板:
int a[10];
std::cout << len(a); // OK: only len() for array matches
std::cout << len("tmp"); // OK: only len() for array matches
当传递一个 std::vector<>
时,匹配第二个函数模板:
std::vector<int> v;
std::cout << len(v); // OK: only len() for a type with size_type matches
当传递一个原始数组时,两个模板都不匹配:第一个模板入参是数组,显然不匹配;第二个模板返回值要求 T
类型有 size_type
成员,显然也不匹配。由于 SFINAE 特性,替换过程不会产生报错,但是编译器会产生 len()
没有找到的报错。
int* p;
std::cout << len(p); // ERROR: no matching len() function found
当传一个 std::allocator
时,std::allocator
有 size_type
成员,第二个函数模板匹配成功,但是 std::allocator
没有 size()
成员,这个时候第二个函数模板不会被忽略,编译器会产生没有 size()
函数的报错。
std::allocator<int> x;
std::cout << len(x); // ERROR: len() function found, but can’t size()
如果增加一个更加通用的 len()
函数:
// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{
return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{
return t.size();
}
// fallback for all other types:
std::size_t len (...)
{
return 0;
}
这里新增的第三个通用 len()
函数,总是匹配的,但是最差的匹配。例如:
int a[10];
std::cout << len(a); // OK: len() for array is best match
std::cout << len("tmp"); // OK: len() for array is best match
std::vector<int> v;
std::cout << len(v); // OK: len() for a type with size_type is best match
int* p;
std::cout << len(p); // OK: only fallback len() matches
std::allocator<int> x;
std::cout << len(x); // ERROR: 2nd len() function matches best,
// but can’t call size() for x
对于原始指针,只有第三个 len()
是匹配的。但是对于 std::allocator
,第二个和第三个函数都匹配,但是第二个更加匹配,因此,编译器还会报没有 size()
的错误。
对于 传递 std::allocator
时,如果我们不想让第二个函数模板匹配,也就是说对于有 size_type 成员而没有 size() 成员的类型,在替换过程中忽略第二个成员函数,一种有效处理如下:
decltype
和逗号操作符定义返回值类型,例如,将第二个函数模板修改如下:
template<typename T>
auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() )
{
return t.size();
}
传递 std::allocator
将不会匹配第二个函数模板,这时只有第三个通用的 len()
匹配:
std::allocator<int> x;
std::cout << len(x);
借助偏特化、SFINAE 和 std::enable_if
,我们可以使能或者使无效模板实现。C++17 引入的编译时 if 语句可以根据编译时条件使能或者使无效特定的语句。例如,在可变函数模板中介绍的 print()
的例子,为了减少一个空参数的 print()
(为了结束递归):
template<typename T, typename... Types>
void print (T const& firstArg, Types const&... args)
{
std::cout << firstArg << ’\n’;
if constexpr(sizeof...(args) > 0) {
print(args...); // code only available if sizeof...(args)>0 (since C++17)
}
}
当只有一个实参传入 print()
时,参数包 args
为空,也即 sizeof...(args)
为 0,递归调用 print()
的代码不会被实例化,递归结束。注意: if constexpr
也可以用于非模板的普通函数中。
至此,本文结束,更多 C++ 模板相关介绍,敬请期待!
参考: