首先我们要明白什么是非类型模板参数(Non-type template parameters):
非类型模板形参(Non-type template parameters)是具有固定类型的模板形参,用于作为模板实参传入的constexpr值的占位符。
说得有点拗口,那么说直接点,Non-type template parameters 是 C++ 模板中的一种特性,它允许你将常量值如下:
- 如整数
- 枚举
- 指向函数的指针或引用
- 指向对象的指针或引用
- std::nullptr_t
- 浮点类型(C++20之后)
- 指向成员函数的指针或引用
- String等类(C++20之后)
作为模板参数,而不是类型。这样,你可以在编译时确定一些代码的细节,而不是在运行时。
也就是你可以把一些程序的细节放在编译环节而不是运行环节,这带来的好处之一就是可以把错误放在编译期解决或者性能优化在编译期解决,C++现在在20标准之后越来越注重编译期解决安全错误,有点像Rust。
我们看一个最直接的例子
#include <iostream>
template <int N> //声明一个非类型模板参数
void print()
{
std::cout << N << std::endl; // 使用n在这里
}
int main()
{
print<5>(); // 5 是一个非类型模板参数
return 0;
}
输出的结果就是5,在实例化阶段,编译器帮我们类似于写了一个如下的函数:
template <>
void print<5>()
{
std::cout << 5 << std::endl;
}
这就是把5放到了编译期而不是运行期。
而在很多细节方面,这是有很大差异的!
举例子,我们想要使用static_assert在编译期进行编译判断,而我们传入的参数是一个变量该怎么办,我们都知道static_assert必须传入常量。
比如我们想这样子:
#include <cassert>
#include <iostream>
double get_double_val(double d)
{
assert(d >= 0.0 && "getSqrt(): d must be non-negative");
if (d >= 0)
return d*2;
return 0.0;
}
int main()
{
std::cout << get_double_val(5.0) << '\n';
std::cout << get_double_val(-5.0) << '\n';
return 0;
}
这里的代码是进行运行期判断,如果我们想在编译期解决这个问题呢?比如我们设想这样子呢(如下)
20 double get_double_val(double D)
21 {
22 static_assert(D >= 0.0, "get_double_val(): D must be non-negative");
23
24 if (D >= 0)
25 return D*2;
26 return 0.0;
27 }
28
29 int main()
30 {
31 std::cout << get_double_val(5.0) << '\n';
32 std::cout << get_double_val(-5.0) << '\n';
33
34 return 0;
35 }
尝试运行一下,编译器提醒我们:
E0028 表达式必须含有常量值 参数 "D" (已声明 所在行数:20) 的值不可用作常量
这是因为static_assert在编译期进行执行,而我们的代码在运行期执行,也就是5.0在运行期传入,晚于编译期。
在C++20之后的标准中,从C++20开始,函数形参可以是constexpr,这是一个新的语法糖,可以让编译器在编译期计算函数的参数值,从而提高性能和简化代码(值得注意的是,这里的constexpr函数形参不能是引用或指针,必须是数值或者文本类型)。
那么我们能不能使用C++20的语法糖,传入呢?
在C++20中,我们能够这样子
constexpr int add(int x, int y) {
return x + y;
}
constexpr int a = 10;
constexpr int b = 20;
constexpr int c = add(a, b); // 编译期计算
int d = add(30, 40); // 运行时计算
我们能不能直接使用constexpr让参数变成const常量传入呢?比如下面这样子
constexpr double get_double_val(double D)
{
static_assert(D >= 0.0, "get_double_val(): D must be non-negative");
if (D >= 0)
return D*2;
return 0.0;
}
int main()
{
constexpr double val = 5.0;
std::cout << get_double_val(5.0) << '\n';
//std::cout << get_double_val(-5.0) << '\n';
return 0;
}
遗憾的是不行,还是出现了这个表达式必须含有常量值的错误,而我们如果使用下面这种方式
constexpr double get_double_val(double D)
{
assert(D >= 0.0, "get_double_val(): D must be non-negative");
if (D >= 0)
return D*2;
return 0.0;
}
int main()
{
constexpr double val = 5.0;
static_assert(get_double_val(5.0)==10);
//std::cout << get_double_val(-5.0) << '\n';
return 0;
}
则是可以运行的。也就是说,目前为止static_assert不能在计算时候进行,哪怕是编译期的计算也不行,所以我们想让static_assert参与运算该怎么办呢?
如果我们将函数形参改为非类型模板形参,那么我们就可以完全按照我们的想法做:
#include <iostream>
template <double D> // 需要C++20
double get_double_val()
{
static_assert(D >= 0.0, "get_double_val(): D must be non-negative");
if (D >= 0)
return D*2;
return 0.0;
}
int main()
{
std::cout << get_double_val<5.0>() << '\n';
std::cout << get_double_val<-5.0>() << '\n';
return 0;
}
代码完美运行!static_assert其作用了!编译器的报错为:
错误 C2338 static_assert failed: 'get_double_val(): D must be non-negative'
现在我们知道了Non-type template parameters的作用了!
我们都知道C++11之后,auto越来越强大,语法糖越来越多,那么在这里也是一样的!从c++ 17开始,非类型模板形参可以使用auto让编译器从模板实参中推断出非类型模板形参
#include <iostream>
template <auto N>
void print()
{
std::cout << N << '\n';
}
int main()
{
print<5>(); // N 作为 int `5`
print<'c'>(); // N 作为 char `c`
return 0;
}
而在这之前,我们需要这样子写:
#include <iostream>
template <int N>
void print1()
{
std::cout << N << '\n';
}
template <char N>
void print2()
{
std::cout << N << '\n';
}
int main()
{
print1<5>(); // N deduced as int `5`
print2<'c'>(); // N deduced as char `c`
return 0;
}
不仅仅是我们要重复构造两个Non-type template parameters,而且我们的函数还不能一致,因为区分函数重载的只有函数参数个数,参数类型,而我们这里的参数都是空的,所以不能使用两个print函数,所以这样子看代码就写的很丑陋。auto很好的解决了这个问题!
另外注意:非类型模板参数也有进行隐式转换
#include <iostream>
template <int N> // int non-type template parameter
void print()
{
std::cout << N << '\n';
}
int main()
{
print<5>(); //无隐式转换
print<'c'>(); // 'c' 隐式转换为 int
return 0;
}