非类型模板形参

在前面的课程中,我们讨论了如何创建使用类型模板形参的函数模板。类型模板形参充当将来由模板实参替换的类型的占位符。

尽管类型模板形参最常用,但还有另一种值得了解的模板形参:非类型模板形参

非类型模板形参

非类型模板形参是具有固定类型的模板形参,用于占位一个以 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>() 时应编译失败。

(参考答案略)

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"