11-9 非类型模板形参(review)

在之前的课程中,我们讨论了如何创建使用类型模板形参的函数模板。类型模板形参作为占位符,用于接收作为模板实参传递的实际类型。

虽然类型模板形参是目前最常用的模板形参类型,但还有另一种值得了解的模板形参:非类型模板形参。


非类型模板形参

非类型模板形参non-type template parameter是一种具有固定类型的模板形参,用于作为常量表达式值的占位符,该值作为模板实参传递。

非类型模板形参可为以下任意类型:

  • 整数的类型
  • 枚举类型
  • std::nullptr_t
  • 浮点类型(自C++20起)
  • 对象的指针或引用
  • 函数的指针或引用
  • 成员函数的指针或引用
  • 字面类类型(自C++20起)

我们在第O.1课——通过std::bitset实现位标志与位操作讨论std::bitset时首次接触了非类型模板形参:

#include <bitset>

int main()
{
    std::bitset<8> bits{ 0b0000'0101 }; // The <8> is a non-type template parameter

    return 0;
}

在std::bitset中,非类型模板形参用于指定存储位数。


定义自定义非类型模板形参

以下是一个使用 int 类型非类型模板形参的函数示例:

#include <iostream>

template <int N> // declare a non-type template parameter of type int named N
void print()
{
    std::cout << N << '\n'; // use value of N here
}

int main()
{
    print<5>(); // 5 is our non-type template argument

    return 0;
}

该示例输出:

image

第 3 行声明了模板形参。尖括号内定义名为 N 的非类型模板形参,作为 int 类型值的占位符。在 print() 函数中使用 N 的值。

第11行调用print()函数时,将整数值5作为非类型模板实参传递。编译器看到此调用时,会实例化类似以下形式的函数:

template <>
void print<5>()
{
    std::cout << 5 << '\n';
}

运行时,当main函数调用此函数时,会输出5。

随后程序结束。很简单,对吧?

正如 T 通常用作第一个类型模板形参的名称,N 惯例上被用作 int 非类型模板形参的名称。

最佳实践
将 N 用作 int 非类型模板形参的名称。


非类型模板形参有何用途?

在C++20中,函数形参不能是constexpr。这适用于普通函数、constexpr函数(这很合理,因为它们必须能在运行时执行),甚至包括consteval函数(这可能令人意外)。

假设我们有如下函数:

#include <cassert>
#include <cmath> // for std::sqrt
#include <iostream>

double getSqrt(double d)
{
    assert(d >= 0.0 && "getSqrt(): d must be non-negative");

    // The assert above will probably be compiled out in non-debug builds
    if (d >= 0)
        return std::sqrt(d);

    return 0.0;
}

int main()
{
    std::cout << getSqrt(5.0) << '\n';
    std::cout << getSqrt(-5.0) << '\n';

    return 0;
}

image

运行时调用 getSqrt(-5.0) 将触发运行时断言。虽然这比完全没有检查要好——因为 -5.0 是字面量(隐式为 constexpr)——但若能通过 static_assert 在编译时捕获此类错误会更理想。然而 static_assert 要求常量表达式,而函数形参不能是 constexpr...

然而,如果我们将函数形参改为非类型模板形参,就能实现预期效果:

#include <cmath> // for std::sqrt
#include <iostream>

template <double D> // requires C++20 for floating point non-type parameters
double getSqrt()
{
    static_assert(D >= 0.0, "getSqrt(): D must be non-negative");

    if constexpr (D >= 0) // ignore the constexpr here for this example
        return std::sqrt(D); // strangely, std::sqrt isn't a constexpr function (until C++26)

    return 0.0;
}

int main()
{
    std::cout << getSqrt<5.0>() << '\n';
    std::cout << getSqrt<-5.0>() << '\n';

    return 0;
}

image

此版本无法编译。当编译器遇到 getSqrt<-5.0>() 时,它将实例化并调用类似以下形式的函数:

template <>
double getSqrt<-5.0>()
{
    static_assert(-5.0 >= 0.0, "getSqrt(): D must be non-negative");

    if constexpr (-5.0 >= 0) // ignore the constexpr here for this example
        return std::sqrt(-5.0);

    return 0.0;
}

image

static_assert 条件为假,因此编译器发出断言。

关键要点
非类型模板形参主要用于向函数(或类类型)传递 constexpr 值,以便在需要常量表达式的上下文中使用这些值。
类类型 std::bitset 使用非类型模板形参来定义存储的位数,因为位数必须是 constexpr 值。

作者注
不得不使用非类型模板形参来规避函数参数不能是 constexpr 的限制,这种做法并不理想。目前有不少不同的提案正在评估中,旨在解决此类情况。我预计在未来的 C++ 语言标准中,我们可能会看到更优的解决方案。


非类型模板实参的隐式转换(可选)

某些非类型模板实参可通过隐式转换匹配不同类型的非类型模板形参。例如:

#include <iostream>

template <int N> // int non-type template parameter
void print()
{
    std::cout << N << '\n';
}

int main()
{
    print<5>();   // no conversion necessary
    print<'c'>(); // 'c' converted to type int, prints 99

    return 0;
}

这将输出:

image

在上例中,'c'被转换为整型以匹配函数模板print()的非类型模板形参,随后该函数将值作为整型进行输出。

在此上下文中,仅允许特定类型的constexpr转换。最常见的允许转换类型包括:

  • 整数的提升(例如 char 到 int)
  • 整数的转换(例如 char 到 long 或 int 到 char)
  • 用户定义转换(例如某个程序定义的类到 int)
  • 左值到右值转换(例如某个变量 x 到 x 的值)

请注意,此列表的限制比列表初始化允许的隐式转换类型更为严格。例如,你可以使用 constexpr int 对 double 类型的变量进行列表初始化,但 constexpr int 非类型模板实参无法转换为 double 非类型模板形参。

允许转换的完整列表可在此处的“转换常量表达式”小节中找到。

与普通函数不同,函数模板调用与定义的匹配算法并不复杂,且不会根据所需转换类型(或无转换需求)对匹配结果进行优先级排序。这意味着当函数模板针对不同非类型模板形参重载时,极易导致模糊匹配:

#include <iostream>

template <int N> // int non-type template parameter
void print()
{
    std::cout << N << '\n';
}

template <char N> // char non-type template parameter
void print()
{
    std::cout << N << '\n';
}

int main()
{
    print<5>();   // ambiguous match with int N = 5 and char N = 5
    print<'c'>(); // ambiguous match with int N = 99 and char N = 'c'

    return 0;
}

image

令人惊讶的是,这两个print()调用都导致了模糊匹配。


使用auto进行非类型模板形参的类型推导(C++17)

从C++17开始,非类型模板形参可使用auto让编译器从模板实参推导出非类型模板形参:

#include <iostream>

template <auto N> // deduce non-type template parameter from template argument
void print()
{
    std::cout << N << '\n';
}

int main()
{
    print<5>();   // N deduced as int `5`
    print<'c'>(); // N deduced as char `c`

    return 0;
}

这段代码能编译并产生预期结果:

image

对于进阶读者

您可能疑惑为何此例未出现前文示例中的模糊匹配。编译器会优先检索模糊匹配项,若不存在则实例化函数模板。本例仅存在单个函数模板,故不存在模糊情况。

实例化上述函数模板后,程序结构如下:

#include <iostream>

template <auto N>
void print()
{
    std::cout << N << '\n';
}

template <>
void print<5>() // note that this is print<5> and not print<int>
{
    std::cout << 5 << '\n';
}

template <>
void print<'c'>() // note that this is print<`c`> and not print<char>
{
    std::cout << 'c' << '\n';
}

int main()
{
    print<5>();   // calls print<5>
    print<'c'>(); // calls print<'c'>

    return 0;
}

测验时间

问题 #1

编写一个带非类型模板形参的 constexpr 函数模板,该模板返回模板实参的阶乘。当程序执行到 factorial<-3>() 时应编译失败。

// define your factorial() function template here

int main()
{
    static_assert(factorial<0>() == 1);
    static_assert(factorial<3>() == 6);
    static_assert(factorial<5>() == 120);

    factorial<-3>(); // should fail to compile

    return 0;
}

image

显示方案

template <int N>
constexpr int factorial()
{
    static_assert(N >= 0);

    int product { 1 };
    for (int i { 2 }; i <= N; ++i)
        product *= i;

    return product;
}

int main()
{
    static_assert(factorial<0>() == 1);
    static_assert(factorial<3>() == 6);
    static_assert(factorial<5>() == 120);

    factorial<-3>(); // should fail to compile

    return 0;
}
posted @ 2026-03-07 20:34  游翔  阅读(0)  评论(0)    收藏  举报