在前面的课程中,我们讨论了如何创建使用类型模板形参的函数模板。类型模板形参充当将来由模板实参替换的类型的占位符。
尽管类型模板形参最常用,但还有另一种值得了解的模板形参:非类型模板形参。
非类型模板形参
非类型模板形参是具有固定类型的模板形参,用于占位一个以 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>() 时应编译失败。
(参考答案略)
