在前面的课程中,我们讨论了如何创建使用类型模板形参的函数模板。类型模板形参充当将来由模板实参替换的类型的占位符。
尽管类型模板形参最常用,但还有另一种值得了解的模板形参:非类型模板形参。
非类型模板形参
非类型模板形参是具有固定类型的模板形参,用于占位一个以 constexpr
值形式传入的模板实参。
非类型模板形参可以是以下类型之一:
- 整型
- 枚举类型
std::nullptr_t
- 浮点类型(C++20 起)
- 对象指针或引用
- 函数指针或引用
- 成员函数指针或引用
- 字面量类类型(C++20 起)
我们首次见到非类型模板形参是在讨论 std::bitset
时:
#include <bitset>
int main()
{
std::bitset<8> bits{ 0b0000'0101 }; // <8> 是非类型模板实参
}
此处非类型模板形参用于告诉 std::bitset
需要存储多少位。
自定义非类型模板形参
下面示例定义了一个使用 int
非类型模板形参的函数:
#include <iostream>
template <int N> // 声明一个类型为 int 的非类型模板形参 N
void print()
{
std::cout << N << '\n';
}
int main()
{
print<5>(); // 5 作为非类型模板实参
}
输出:
5
- 第 3 行:模板形参声明,将
N
定义为int
类型的非类型形参。 - 第 9 行:调用
print<5>
,编译器实例化:
template<>
void print<5>()
{
std::cout << 5 << '\n';
}
通常用 N
命名 int
非类型模板形参。
最佳实践
使用 N
作为 int
非类型模板形参的名称。
非类型模板形参的用途
截至 C++20,函数形参不能是 constexpr
。因此,若希望函数在编译期接收常量并参与 static_assert
等上下文,可将形参改为非类型模板形参。
示例: 原始函数(运行期检查):
double getSqrt(double d)
{
assert(d >= 0.0);
return std::sqrt(d);
}
改为非类型模板形参(C++20 支持浮点形参):
#include <cmath>
template <double D>
double getSqrt()
{
static_assert(D >= 0.0, "D 必须非负");
return std::sqrt(D); // C++26 前非 constexpr
}
int main()
{
std::cout << getSqrt<5.0>() << '\n'; // OK
getSqrt<-5.0>(); // 编译错误
}
关键洞察
非类型模板形参主要用于把 constexpr
值传给函数/类,使其能在需要常量表达式的上下文中使用。
非类型模板实参的隐式转换(可选)
某些 constexpr
值可隐式转换以匹配非类型模板形参:
template <int N>
void print() { std::cout << N << '\n'; }
int main()
{
print<5>(); // 无需转换
print<'c'>(); // 'c' 转换为 int,输出 99
}
允许转换包括整型提升、整型转换、用户自定义转换等,但比列表初始化更受限。
重载与二义性
若对同一函数名重载不同非类型模板形参,易产生二义性:
template <int N> void print() {}
template <char N> void print() {}
int main()
{
print<5>(); // 二义性
print<'c'>(); // 二义性
}
C++17 的 auto
非类型模板形参
C++17 起可用 auto
让编译器推导非类型模板形参:
template <auto N>
void print() { std::cout << N << '\n'; }
int main()
{
print<5>(); // N 推导为 int
print<'c'>(); // N 推导为 char
}
此时只有一个模板,无二义性。
小测
问题 1
编写一个带非类型模板形参的 constexpr
函数模板,返回形参的阶乘。
要求:调用 factorial<-3>()
时应编译失败。
(参考答案略)