avatar

目录
258 - Non type template parameters

258 - Non Type Template Parameters

https://gemini.google.com/u/2/app/a90fe4536e5091d1

好的,没问题!作为一名经验丰富的 C++ 开发者和热爱教学的老师,我非常乐意用最适合初学者的方式,带你一步步学习 C++ 的 非类型模板参数 [Non-type template parameters]。我们开始吧!


1. 标题与简介 (Title and Summary)

标题: C++ 模板进阶:深入理解非类型模板参数

简介:

本节课我们将学习 C++ 模板中的一个重要概念——非类型模板参数 [Non-type template parameters]。与我们之前熟悉的 类型模板参数 [Type template parameters](如 typename T,代表一个待定的数据类型)不同,非类型模板参数是用来代表一个 值 的占位符,其具体类型在模板声明时就已经确定。我们将探讨它的语法、用途、与类型参数的区别、C++ 版本间的差异,并通过实例来理解它。


2. 详细分步讲解 (Detailed Explanation)

让我们循序渐进地深入了解非类型模板参数:

  • 回顾:类型模板参数 (Type Template Parameters)

    • 首先,回忆一下我们常用的 类型模板参数 [Type template parameters]。在像 template<typename T> 这样的声明中,T 是一个占位符。当我们使用这个模板(比如函数模板或类模板)时,我们需要为 T 指定一个具体的类型,比如 intdouble 或者我们自己定义的类。T 代表的是一个 类型
  • 引入:非类型模板参数 (Non-Type Template Parameters)

    • 现在,我们来看 非类型模板参数 [Non-type template parameters]。顾名思义,它不是用来代表类型的。相反,它代表一个
    • 它的语法是在模板声明中直接指定一个 具体的 C++ 类型,后面跟着参数名,例如:template<int MaxSize, typename T>
    • 在这个例子中,MaxSize 就是一个非类型模板参数。它的类型被明确指定为 int。当我们使用这个模板时,我们需要为 MaxSize 提供一个具体的 int ,比如 100512
    • T 仍然是一个类型模板参数,代表一个待定的 类型
  • 关键区别 (Key Difference)

    • 类型模板参数 (e.g., typename T): 占位符,代表一个 类型。调用时提供具体类型(如 double)。
    • 非类型模板参数 (e.g., int Threshold): 占位符,代表一个 。其类型在模板声明时已固定(如 int)。调用时提供具体的值(如 200)。
  • 示例:is_valid 函数 (Example: is_valid function)

    • 目的: 假设我们需要一个函数来判断一个数组(例如存储温度数据)是否“有效”。有效性的标准是:数组中所有元素的总和必须大于某个预设的阈值。
    • 使用非类型参数的实现:

      C++

      Code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      #include <cstddef> // for std::size_t
      #include <numeric> // 潜在的,如果用 std::accumulate

      template <int Threshold, typename T> // Threshold 是 int 类型的非类型模板参数
      bool is_valid(const T arr[], std::size_t size) {
      T sum = T{}; // 初始化为 T 类型的零值 (e.g., 0 for int/double)
      for (std::size_t i = 0; i < size; ++i) {
      sum += arr[i];
      }
      // 将计算出的总和与模板参数 Threshold (一个编译时已知的值) 进行比较
      return sum > Threshold;
      }
    • 解释:

      1. template <int Threshold, typename T>: 声明了一个模板,它有两个参数。第一个 int Threshold 是非类型参数,类型为 int。第二个 typename T 是类型参数。
      2. bool is_valid(const T arr[], std::size_t size): 函数签名。它接受一个类型为 T 的数组 arr 和它的大小 size
      3. T sum = T{};: 初始化一个和变量 sum,其类型与数组元素类型 T 相同。T{} 是零初始化,对于数字类型通常是 0。
      4. for 循环: 遍历数组,累加所有元素到 sum
      5. return sum > Threshold;: 核心部分。这里将运行时计算出的 sum 与编译时就已确定的 Threshold 值进行比较。
    • 如何调用:

      C++

      Code
      1
      2
      3
      4
      5
      6
      7
      double temperatures[] = {10.5, 20.0, 30.5, 40.0, 50.0};
      std::size_t count = sizeof(temperatures) / sizeof(double); // 或者使用 std::size(temperatures) in C++17

      // 调用 is_valid,为非类型参数 Threshold 提供值 150,为类型参数 T 提供类型 double
      bool result = is_valid<150, double>(temperatures, count);
      // ^^^^^ ^^^^^^
      // 值 类型

      在这个调用中,150 是传递给 Threshold 参数的 double 是传递给 T 参数的 类型。编译器会根据 <150, double> 生成一个特定的 is_valid 函数实例。

  • 实用性与替代方案 (Usefulness and Alternatives)

    • 优点: 将像 Threshold 这样的值“嵌入”到函数(或类)的类型中。这在某些元编程或需要编译时常量的场景下可能有用。
    • 缺点 (原文作者观点): 这种方式可能让函数调用看起来有些笨拙(is_valid<150, double>(...)),参数列表 <...> 中既有类型又有值,可能降低代码的可读性。
    • 替代方案 (更常见): 将阈值作为普通的函数参数传递。

      C++

      Code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      template <typename T> // 只需要类型参数 T
      bool is_valid_alternative(const T arr[], std::size_t size, T threshold) { // threshold 作为普通参数
      T sum = T{};
      for (std::size_t i = 0; i < size; ++i) {
      sum += arr[i];
      }
      return sum > threshold; // 与运行时传入的 threshold 比较
      }

      // 调用方式更直观
      bool result_alt = is_valid_alternative<double>(temperatures, count, 150.0);
      // ^^^^^
      // 阈值作为普通参数

      这种方式通常更灵活,也更容易阅读和理解。

  • 支持的类型 (Supported Types & C++ Version Differences)

    • C++17及更早版本: 对非类型模板参数的类型有限制。通常只支持:
      • 整型 (int, unsigned int, char, bool, long long, etc.)
      • 枚举类型 (enum)
      • 指针类型 (Type*, 包括函数指针和成员指针)
      • 左值引用类型 (Type&)
      • std::nullptr_t
      • 不支持 浮点类型 (float, double) 和大多数类类型。如果你尝试使用 double 作为非类型参数,编译器会报错。
    • C++20: 极大地放宽了限制!现在支持:
      • 所有之前的类型。
      • 浮点类型 (float, double)
      • 字面量类类型 (Literal Class Types): 这是一种特殊的类类型,可以在编译时求值。这超出了我们目前的范围,但知道C++20扩展了对类类型的支持很重要。
    • 注意: 原文提到,作者使用的 GCC 10 编译器还不支持 C++20 的浮点非类型参数,需要 GCC 11 或更高版本(或像 Wandbox 这样的在线编译器提前体验)。
  • 浮点非类型参数示例 (Floating-point Example - C++20)

    • 原文展示了一个 C++20 的例子,使用 double 作为非类型参数:

      C++

      Code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 需要 C++20 和支持该特性的编译器 (如 GCC 11+)
      template <typename T, double Coefficient> // Coefficient 是 double 类型的非类型参数
      T process(T a, T b) {
      // 使用编译时确定的 Coefficient 值
      return a * b - Coefficient;
      }

      // 调用 (假设编译器支持)
      // double result_process = process<double, 5.5>(10.0, 2.0); // 结果会是 10.0 * 2.0 - 5.5 = 14.5
    • 重点: 这个特性依赖于较新的 C++ 标准和编译器。如果在旧环境(如 C++17 或使用不支持的编译器如 GCC 10)下尝试编译,会失败。


3. 代码示例 (Code Example)

这里是本节课讨论的主要代码示例:

示例 1: 使用非类型模板参数的 is_valid 函数

C++

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <cstddef> // for std::size_t
#include <vector> // 使用 vector 更方便管理大小
#include <numeric> // std::accumulate 可以简化求和
#include <iomanip> // for std::boolalpha

// 模板定义:Threshold 是非类型参数,T 是类型参数
template <int Threshold, typename T>
bool is_valid(const T arr[], std::size_t size) {
T sum = T{}; // 初始化为零值
for (std::size_t i = 0; i < size; ++i) {
sum += arr[i];
}
std::cout << " [Debug] Sum is: " << sum << ", Threshold is: " << Threshold << std::endl;
return sum > Threshold;
}

// 更现代化的版本,使用 std::vector 和 std::accumulate
template <int Threshold, typename Container>
bool is_valid_v2(const Container& data) {
// 使用 typename 在依赖类型中访问嵌套类型
using T = typename Container::value_type;
// std::accumulate 需要 <numeric> 头文件
T sum = std::accumulate(data.begin(), data.end(), T{});
std::cout << " [Debug V2] Sum is: " << sum << ", Threshold is: " << Threshold << std::endl;
return sum > Threshold;
}


int main() {
double temperatures[] = {10.0, 20.0, 30.0, 40.0, 50.0}; // Sum = 150
std::size_t count = std::size(temperatures); // C++17

std::cout << std::boolalpha; // 让输出 bool 时显示 true/false 而不是 1/0

// 测试1:阈值设为 200,数组总和 150,应为 false
std::cout << "Testing with Threshold = 200:" << std::endl;
bool result1 = is_valid<200, double>(temperatures, count);
std::cout << "Is the array valid (Threshold 200)? " << result1 << std::endl << std::endl;

// 测试2:阈值设为 100,数组总和 150,应为 true
std::cout << "Testing with Threshold = 100:" << std::endl;
bool result2 = is_valid<100, double>(temperatures, count);
std::cout << "Is the array valid (Threshold 100)? " << result2 << std::endl << std::endl;

// 使用 V2 版本和 std::vector
std::vector<double> temps_vec = {10.0, 20.0, 30.0, 40.0, 50.0, 100.0}; // Sum = 250
std::cout << "Testing V2 with Threshold = 200 and extra element:" << std::endl;
bool result_v2 = is_valid_v2(temps_vec); // T 可以被推导出来
std::cout << "Is the vector valid (Threshold 200)? " << result_v2 << std::endl;

return 0;
}

示例 2: 使用普通函数参数传递阈值的替代方案

C++

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <cstddef>
#include <vector>
#include <numeric>
#include <iomanip>

template <typename T>
bool is_valid_alternative(const T arr[], std::size_t size, T threshold) { // threshold 是普通参数
T sum = T{};
for (std::size_t i = 0; i < size; ++i) {
sum += arr[i];
}
std::cout << " [Debug Alt] Sum is: " << sum << ", Threshold is: " << threshold << std::endl;
return sum > threshold;
}

int main() {
double temperatures[] = {10.0, 20.0, 30.0, 40.0, 50.0}; // Sum = 150
std::size_t count = std::size(temperatures);

std::cout << std::boolalpha;

// 调用替代方案,阈值作为最后一个参数传递
std::cout << "Testing alternative with threshold = 200:" << std::endl;
bool result_alt1 = is_valid_alternative<double>(temperatures, count, 200.0);
std::cout << "Is the array valid (Threshold 200)? " << result_alt1 << std::endl << std::endl;

std::cout << "Testing alternative with threshold = 100:" << std::endl;
bool result_alt2 = is_valid_alternative<double>(temperatures, count, 100.0);
std::cout << "Is the array valid (Threshold 100)? " << result_alt2 << std::endl;

return 0;
}

示例 3: 浮点非类型参数 (C++20, 可能需特定编译器)

C++

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

// 需要 C++20 和支持该特性的编译器 (e.g., GCC 11+, Clang 12+)
template <typename T, double Coefficient> // 非类型参数是 double
T process(T a, T b) {
return a * b - Coefficient;
}

int main() {
// 编译这部分代码需要 C++20 模式和支持的编译器
// 尝试编译:g++ your_code.cpp -std=c++20 -o your_app
/*
double result_process = process<double, 5.5>(10.0, 2.0);
std::cout << "Processing result (10.0 * 2.0 - 5.5): " << result_process << std::endl;
*/
std::cout << "Floating point non-type parameter example requires C++20 and compatible compiler." << std::endl;
std::cout << "Uncomment the code and compile with e.g., g++ -std=c++20 ..." << std::endl;

return 0;
}

非代码类比 (Non-code Analogy):

想象你在设计一个“制作蛋糕”的通用流程模板。

  • 烘焙温度 [Baking Temperature] 可以是一个 非类型模板参数。比如你设计一个模板 <int TempC>,当你实例化这个模板时,你提供一个具体的值,如 ,代表这次蛋糕必须在 180°C 下烘烤。这个温度值在“编译”(准备阶段)时就固定了。
  • 主要干性成分 [Main Dry Ingredient] 可以是一个 类型模板参数。比如 <typename FlourType>。当你实例化时,你可以指定用 低筋面粉 [Cake Flour] 还是 全麦面粉 [Whole Wheat Flour]。这代表的是一个 类型

4. 问答卡片 (Q&A Flash Cards)

  • 问: 什么是非类型模板参数 (Non-type template parameter)?

    答: 它是一种模板参数,代表一个编译时已知的 值,而不是一个类型。它的具体类型在模板声明时就被指定了 (例如 int N, bool Flag)。

  • 问: 非类型模板参数和类型模板参数 (Type template parameter) 的主要区别是什么?

    答: 类型模板参数 (如 typename T) 是 类型 的占位符;非类型模板参数 (如 int Threshold) 是 值 的占位符,其类型是固定的。

  • 问: 在调用使用了非类型模板参数的函数或类模板时,如何提供参数?

    答: 在模板参数列表(尖括号 <>)中,为非类型参数提供具体的值,为类型参数提供具体的类型。例如:my_template<10, double>(…),这里的 10 是给非类型参数的值,double 是给类型参数的类型。

  • 问: 在 C++17 标准中,可以使用 double 或 float 作为非类型模板参数吗?

    答: 不可以。C++17 及更早版本不支持浮点类型作为非类型模板参数。

  • 问: C++20 对非类型模板参数做了哪些重要的扩展?

    答: C++20 允许使用浮点类型 (float, double) 以及满足特定条件的类类型(字面量类类型)作为非类型模板参数。


5. 常见误解/错误 (Common Misunderstandings/Mistakes)

  1. 类型与值混淆 (Confusing Types and Values):

    • 错误:试图为非类型参数提供类型,或为类型参数提供值。

      C++

      Code
      1
      2
      3
      // 错误示例
      // is_valid<double, 200>(arr, size); // 顺序反了,200 是值,double 是类型
      // is_valid<MyClass, double>(arr, size); // 假设 MyClass 是一个值,但这里需要 int
    • 正确:is_valid<200, double>(arr, size); (值在前,类型在后,符合模板定义 template <int Threshold, typename T>)

  2. 使用 C++17 不支持的类型 (Using Unsupported Types in C++17):

    • 错误:在 C++17 或更早版本的编译器环境下,尝试使用 double, float 或不符合要求的类类型作为非类型模板参数。

      C++

      Code
      1
      2
      3
      // 在 C++17 下编译会报错
      // template <double MaxValue, typename T> // 错误: C++17 不支持 double
      // void process_data(...) {}
    • 正确 (C++17): 只能使用整型、枚举、指针、引用等允许的类型。

    • 正确 (C++20): 可以使用 double 等,但需确保编译器支持 C++20 标准。
  3. 过度设计 (Over-engineering):

    • 误解:认为任何编译时常量都应该用非类型模板参数。
    • 实践:如果一个值逻辑上更像是函数的配置参数,并且不需要它成为类型系统的一部分,那么使用普通的函数参数通常更清晰、更灵活。如 is_valid 例子中,将 threshold 作为函数参数传递通常是更好的选择。
  4. 忘记提供模板参数 (Forgetting Template Arguments):

    • 错误:调用需要显式指定非类型参数的模板函数时,忘记了 <...> 部分。

      C++

      Code
      1
      // 错误: is_valid(temperatures, count); // 编译器无法推断 Threshold 的值
    • 正确:is_valid<150, double>(temperatures, count); (显式提供模板参数)

      • 注意: 类型参数 T 有时可以被编译器从函数参数中推导出来(如 is_valid_v2(temps_vec);T 可以从 temps_vec 推导为 double),但非类型参数通常需要显式指定,除非有默认模板参数。

6. 编程练习 (Coding Exercise)

现在,让我们来做个小练习巩固一下。下面是一个函数模板 print_repeatedly 的骨架,它应该接受一个整数 N 作为非类型模板参数,以及一个类型 T 作为类型模板参数。函数的功能是接收一个 T 类型的值 message,并将其打印输出 N 次。

请你填充 /* ??? */ 部分的代码,使其能够正确编译和运行。

C++

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <string>

// TODO: 请填充模板声明部分
/* ??? */
void print_repeatedly(T message) {
for (int i = 0; i < N; ++i) { // 使用非类型参数 N 作为循环次数
std::cout << message << std::endl;
}
}

int main() {
// TODO: 填充函数调用部分,使其打印 "Hello" 3 次
/* ??? */

std::cout << "---" << std::endl;

// TODO: 填充函数调用部分,使其打印整数 42 两次
/* ??? */

return 0;
}

请尝试完成 /* ??? */ 部分的代码。完成后,请将你的答案发给我,我会进行评估并给出反馈。

作业:

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>

#include <string>



// TODO: 请填充模板声明部分

/* ??? */

template <int N, typename T>

void print_repeatedly(T message) {

    for (int i = 0; i < N; ++i) {  // 使用非类型参数 N 作为循环次数

        std::cout << message << std::endl;

    }

}



int main() {

    // TODO: 填充函数调用部分,使其打印 "Hello" 3 次

    print_repeatedly<3, std::string>(

        std::string{"Hello"});  // N=3, T deduced as const char*



    std::cout << "---" << std::endl;



    // TODO: 填充函数调用部分,使其打印整数 42 两次

    /* ??? */

    print_repeatedly<2, int>(42);



    return 0;

}

好的,我们来看看你完成的练习!


7. 评价你的答案与讲解 (Rating Your Solution & Explanation)

评价: 非常好 (Excellent)! 👍

你的代码完全正确地实现了练习要求。

  • 模板声明 (template <int N, typename T>): 你准确地定义了一个接受 int 类型非类型参数 N 和一个类型参数 T 的模板。这完全符合我们的学习目标。
  • 第一次调用 (print_repeatedly<3, std::string>(std::string{"Hello"});):

    • 你正确地在尖括号 <> 中为非类型参数 N 提供了值 3,并为类型参数 T 显式指定了类型 std::string
    • 传递的参数 std::string{"Hello"} 与你指定的 T=std::string 类型匹配。
    • (一个小说明:你代码旁的注释 // N=3, T deduced as const char* 稍微有点偏差。因为你显式指定了 <..., std::string>,所以 T 在这里就是 std::string,而不是通过推导得到的 const char*。如果你写成 print_repeatedly("Hello");,那么 T 才会确实被推导为 const char*。不过,你写的代码本身是完全正确的,只是注释内容可以更精确些。)
  • 第二次调用 (print_repeatedly<2, int>(42);):

    • 同样,你正确地为 N 提供了值 2,为 T 指定了类型 int
    • 传递的参数 42 (一个 int 字面量) 与 T=int 匹配。

总的来说,你清晰地展示了如何定义带有非类型和类型模板参数的函数模板,以及如何在调用时显式地为它们提供参数。做得很好!

标准答案与你的代码一致:

C++

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <string>

// 正确的模板声明
template <int N, typename T>
void print_repeatedly(T message) {
for (int i = 0; i < N; ++i) { // 使用非类型参数 N
std::cout << message << std::endl;
}
}

int main() {
// 正确的调用,显式指定 N=3, T=std::string
print_repeatedly<3, std::string>(std::string{"Hello"});

std::cout << "---" << std::endl;

// 正确的调用,显式指定 N=2, T=int
print_repeatedly<2, int>(42);

return 0;
}

工作原理回顾:

当你调用 print_repeatedly<3, std::string>(…) 时,编译器会根据 <3, std::string> 这个“指令”生成一个特定版本的 print_repeatedly 函数,其中 N 被替换为 3,T 被替换为 std::string。然后,这个生成的函数会用你传递的 “Hello” 字符串作为 message 参数来执行。第二次调用同理。


8. 建议和扩展问题 (Suggestions & Extended Questions)

为了让你更深入地探索模板,这里有一些建议和可以思考的问题:

  • 建议1:探索类型推导 (Explore Type Deduction)

    • 试试看,如果你在调用时不显式指定类型参数 T 会怎样?

      C++

      Code
      1
      2
      print_repeatedly("World"); // T 会被推导成什么? (提示: const char*)
      print_repeatedly(123.45); // T 会被推导成什么? (提示: double)
    • 思考一下,为什么非类型参数 N 通常不能被推导,而必须显式指定? (提示:它不直接对应于函数参数的类型)

  • 建议2:默认模板参数 (Default Template Arguments)

    • 模板参数也可以有默认值,就像函数参数一样。

      C++

      Code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      template <int N = 1, typename T = std::string> // N 默认是 1, T 默认是 std::string
      void print_default(T message = "Default Message") {
      for (int i = 0; i < N; ++i) {
      std::cout << message << std::endl;
      }
      }
      // 如何调用 print_default() 来使用默认值?
      // print_default<>(); // 编译通过吗?会打印什么?
      // print_default(); // 打印什么?
      // print_default("Custom"); // 打印什么?
  • 建议3:了解 C++20 的 auto 非类型参数 (Learn about C++20 auto Non-type Parameters)

    • 在 C++20 中,你可以用 auto 关键字让编译器自动推断非类型参数的类型:

      C++

      Code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // C++20 only
      template <auto Value, typename T>
      void process_value(T data) {
      std::cout << "Value: " << Value << ", Data: " << data << std::endl;
      // 'Value' 的类型由调用时提供的值决定
      }
      // process_value('A'); // Value 是 int 类型
      // process_value<true>('B'); // Value 是 bool 类型
      // process_value<'X'>('C'); // Value 是 char 类型
    • 这使得模板更加灵活,但需要 C++20 的编译器支持。

  • 扩展问题1:

    • 你能修改 print_repeatedly 模板,让重复次数 N 有一个默认值,比如默认打印 1 次吗?该如何修改模板声明?
  • 扩展问题2:

    • 尝试编写一个函数模板 create_array。它接受一个 int Size (非类型参数) 和一个 typename T (类型参数)。函数需要创建并返回一个 std::array<T, Size> 类型的数组(需要包含 <array> 头文件)。数组元素可以用 T{} (零初始化/默认初始化) 来初始化。
    • 调用示例:auto my_arr = create_array<5, int>(); 应该能得到一个包含 5 个 int(初始值为 0)的 std::array
  • 扩展问题3 (挑战):

    • 回顾一下 C++17 和 C++20 对非类型模板参数类型的限制。如果你试图将 print_repeatedly 的非类型参数 N 定义为 double (即 template <double N, typename T>),然后在 C++17 环境下调用 print_repeatedly<2.5, std::string>("Test"); 会发生什么?在 C++20 环境下呢?为什么会有差异?

希望这些建议和问题能帮助你更好地理解和运用 C++ 模板!继续努力,你在 C++ 的学习之路上会越走越远!


评论