avatar

目录
257 - cpp Default template arguments

257 - Cpp Default Template Arguments

好的,各位C++的新同学!我是你的专属C++老师,非常高兴能带你进入C++的世界。今天,我们将一起学习一个C++模板中非常实用的特性:函数模板 (Function Template)默认模板参数 (Default Template Arguments)。我会用最清晰、最细致的方式,一步步带你掌握它。

https://gemini.google.com/u/2/app/9c41ec259a7a8242

1. 标题和摘要

标题: C++ 函数模板:轻松掌握默认模板参数

摘要: 本节课我们将学习如何在C++函数模板中使用默认参数。这允许我们在调用模板函数时省略一些模板参数,编译器会使用预设的默认值,特别是对于难以自动推导的返回类型,这非常有用,能让我们的代码更简洁、更灵活。


2. 详细解释

让我们一步步来理解函数模板的默认参数:

  • 什么是函数模板 (Function Template)?

    • 想象一下,你想写一个函数来比较两个数的大小,找出较大的那个。你可能需要为整数 (int)、小数 (double)、甚至其他类型都写一个版本。
    • 函数模板就像一个“函数蓝图”,你只需要写一次这个“蓝图”,编译器就可以根据你调用函数时提供的具体类型,自动生成对应类型的函数版本。这大大减少了重复代码。
    • 我们使用 template <typename T, typename P, ...> 这样的语法来声明模板,其中 TP 就是 模板参数 (template parameters),它们是类型的占位符。
  • 为什么需要默认模板参数?

    • 在某些情况下,编译器可以根据你调用函数时传入的 实参 (arguments) 类型来推断模板参数应该是什么具体类型。例如,maximum(5, 10),编译器能推断出你想用 int 类型。
    • 但是,返回类型 (return type) 通常是编译器无法自动推断的。看下面的模板:

      C++

      Code
      1
      2
      3
      4
      template <typename RetT, typename T, typename P>
      RetT maximum(T a, P b) {
      return (a > b) ? a : b;
      }

      如果你直接调用 maximum(5, 9.5),编译器知道 TintPdouble,但它不知道你希望返回 int 还是 double (也就是 RetT 应该是什么)。这时,编译器会报错。

    • 以前,我们必须 显式指定 (explicitly specify) 模板参数,像这样:maximum<double>(5, 9.5),明确告诉编译器返回类型是 double
    • 默认模板参数 提供了一种更便捷的方式。我们可以为模板参数预设一个默认值。如果调用函数时没有显式指定这个参数,编译器就会使用这个默认值。
  • 如何设置默认模板参数?

    • template 声明语句中,给模板参数赋值即可。
    • 示例1:返回类型作为第一个模板参数,并设置默认值

      C++

      Code
      1
      2
      3
      4
      template <typename RetT = double, typename T, typename P> // RetT 默认为 double
      RetT maximum(T a, P b) {
      // ... 函数体 ...
      }

      这里,我们为 RetT 设置了默认值 double

    • 示例2:返回类型作为最后一个模板参数,并设置默认值

      C++

      Code
      1
      2
      3
      4
      template <typename T, typename P, typename RetT = double> // RetT 默认为 double
      RetT minimum(T a, P b) {
      // ... 函数体 ...
      }

      这里,RetT 同样默认为 double注意一个规则: 通常,如果你为一个模板参数设置了默认值,那么它后面的所有模板参数 也必须 有默认值。但在函数模板中,规则稍有不同:只要编译器能够通过函数调用参数推断出没有默认值的模板参数,或者所有未指定的参数都有默认值,就可以了。将有默认值的参数(尤其是返回类型这种无法推断的)放在后面是更常见的做法。

  • 使用默认参数调用模板函数:

    • 完全省略模板参数: 如果所有无法从函数调用参数中推断出来的模板参数都有默认值(比如返回类型),你就可以完全不写尖括号 <> 来调用。

      C++

      Code
      1
      2
      3
      4
      // 使用上面示例1的 maximum 模板
      auto result = maximum(5, 9.5); // 正确!编译器看到没有指定 RetT,
      // 使用默认的 double 作为返回类型。
      // T 被推断为 int, P 被推断为 double.
    • 显式指定部分参数: 你仍然可以显式指定参数。如果你指定了,那么你提供的值会 覆盖 (override) 默认值。

      C++

      Code
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 使用上面示例1的 maximum 模板
      auto result_int = maximum<int>(5, 9.5); // 显式指定 RetT 为 int,覆盖了默认的 double。
      // T 推断为 int, P 推断为 double.
      // 返回前会将比较结果从 double 转换成 int。

      auto result_all = maximum<int, double, double>(5, 9.5); // 显式指定所有参数
      // RetT = int, T = double, P = double
      // 传入的 5 (int) 会被转换成 double (T)
      // 传入的 9.5 (double) 类型匹配 P
      // 比较结果 (double) 返回前会转换成 int (RetT)
    • 参数推导和指定混合: 当你显式指定部分参数时,编译器会尝试推断剩下的参数。

      • 对于 maximum<int>(5, 9.5),你指定了第一个参数 RetTint。编译器会根据函数调用 maximum(5, 9.5) 推断出 TintPdouble
      • 对于 minimum<int>(6.2, 3) (使用上面示例2的minimum模板),你指定了第一个参数 Tint。编译器会根据调用推断出 Pint。因为 RetT 有默认值 double 且你没有指定它,所以返回类型将使用默认的 double
  • 类型转换 (Type Conversion) 的注意事项:

    • 当模板参数类型(无论是显式指定、默认值还是推导出的)与传入的实参类型或期望的返回类型不完全匹配时,编译器会尝试进行 隐式类型转换 (implicit type conversion)
    • 扩展转换 (Widening Conversion):从小范围类型转到大范围类型(如 intdouble),通常是安全的。
    • 窄化转换 (Narrowing Conversion):从大范围类型转到小范围类型(如 doubleint),可能会丢失精度(小数部分被截断)。这在返回时尤其要注意,如 maximum<int>(5, 9.5) 的例子,虽然 9.5 更大,但因为返回类型被强制为 int,最终返回的是 9
  • 声明与定义分离:

    • (虽然例子中没有体现)默认参数的一个好处是,它有助于将模板的声明(放在头文件中)和定义(可以放在源文件中,虽然模板通常定义也放在头文件里)分开,因为编译器在看到声明时就已经知道了默认值,不需要等到看到定义。但对于模板,最常见的做法还是将声明和定义都放在头文件中。

3. 代码示例

下面是结合了视频中 maximumminimum 函数的完整示例代码:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <typeinfo> // 用于 typeid
#include <iomanip> // 用于 std::fixed, std::setprecision (如果需要精确打印浮点数)

// 示例 1: 返回类型作为第一个模板参数,并设置默认值为 double
// T 和 P 可以通过函数调用参数推断出来
template <typename RetT = double, typename T, typename P>
RetT maximum(T a, P b) {
std::cout << " [maximum 函数内部] 参数 a 类型: " << typeid(T).name()
<< ", 参数 b 类型: " << typeid(P).name() << std::endl;
RetT result = (a > b) ? static_cast<RetT>(a) : static_cast<RetT>(b); // 显式转换以匹配返回类型
std::cout << " [maximum 函数内部] 计算结果类型: " << typeid(decltype((a > b) ? a : b)).name()
<< ", 返回类型 RetT: " << typeid(RetT).name() << std::endl;
return result;
}

// 示例 2: 返回类型作为最后一个模板参数,并设置默认值为 double
// T 和 P 可以通过函数调用参数推断出来
template <typename T, typename P, typename RetT = double>
RetT minimum(T a, P b) {
std::cout << " [minimum 函数内部] 参数 a 类型: " << typeid(T).name()
<< ", 参数 b 类型: " << typeid(P).name() << std::endl;
RetT result = (a < b) ? static_cast<RetT>(a) : static_cast<RetT>(b); // 显式转换以匹配返回类型
std::cout << " [minimum 函数内部] 计算结果类型: " << typeid(decltype((a < b) ? a : b)).name()
<< ", 返回类型 RetT: " << typeid(RetT).name() << std::endl;
return result;
}

int main() {
int a_int = 5;
double b_double = 9.5;
int c_int = 6;

std::cout << "--- 调用 maximum (RetT 默认 double) ---" << std::endl;
// 1. 完全不指定模板参数,RetT 使用默认的 double
std::cout << "1. 调用 maximum(" << a_int << ", " << b_double << ") - 无显式参数:" << std::endl;
auto result1 = maximum(a_int, b_double); // RetT=double (默认), T=int, P=double
std::cout << " 结果: " << result1 << ", 结果类型: " << typeid(result1).name()
<< ", 大小: " << sizeof(result1) << " bytes" << std::endl << std::endl;

// 2. 显式指定第一个参数 (RetT),覆盖默认值
std::cout << "2. 调用 maximum<int>(" << a_int << ", " << b_double << ") - 显式指定 RetT=int:" << std::endl;
auto result2 = maximum<int>(a_int, b_double); // RetT=int (覆盖默认), T=int, P=double
// 内部比较结果是 double (9.5),返回时窄化转换为 int (9)
std::cout << " 结果: " << result2 << ", 结果类型: " << typeid(result2).name()
<< ", 大小: " << sizeof(result2) << " bytes" << std::endl << std::endl;

// 3. 显式指定所有参数
std::cout << "3. 调用 maximum<int, double, double>(" << a_int << ", " << b_double << ") - 显式指定所有参数:" << std::endl;
auto result3 = maximum<int, double, double>(a_int, b_double); // RetT=int, T=double, P=double
// 传入的 a_int(5) 会转为 T(double)
// 内部比较结果是 double(9.5),返回时转为 RetT(int) -> 9
std::cout << " 结果: " << result3 << ", 结果类型: " << typeid(result3).name()
<< ", 大小: " << sizeof(result3) << " bytes" << std::endl << std::endl;


std::cout << "\n--- 调用 minimum (RetT 默认 double) ---" << std::endl;
// 4. 完全不指定模板参数,RetT 使用默认的 double
std::cout << "4. 调用 minimum(" << c_int << ", " << b_double << ") - 无显式参数:" << std::endl;
auto result4 = minimum(c_int, b_double); // T=int, P=double, RetT=double (默认)
std::cout << " 结果: " << result4 << ", 结果类型: " << typeid(result4).name()
<< ", 大小: " << sizeof(result4) << " bytes" << std::endl << std::endl;

// 5. 显式指定前两个参数 (T, P),RetT 使用默认值
std::cout << "5. 调用 minimum<int, double>(" << c_int << ", " << b_double << ") - 指定 T=int, P=double:" << std::endl;
auto result5 = minimum<int, double>(c_int, b_double); // T=int, P=double, RetT=double (默认)
// 传入参数类型匹配 T 和 P
// 返回默认的 double 类型
std::cout << " 结果: " << result5 << ", 结果类型: " << typeid(result5).name()
<< ", 大小: " << sizeof(result5) << " bytes" << std::endl << std::endl;

// 6. 显式指定所有参数,覆盖 RetT 的默认值
std::cout << "6. 调用 minimum<int, double, int>(" << c_int << ", " << b_double << ") - 指定 T=int, P=double, RetT=int:" << std::endl;
auto result6 = minimum<int, double, int>(c_int, b_double); // T=int, P=double, RetT=int (覆盖默认)
// 内部比较结果是 double (6.0),返回时转为 RetT(int) -> 6
std::cout << " 结果: " << result6 << ", 结果类型: " << typeid(result6).name()
<< ", 大小: " << sizeof(result6) << " bytes" << std::endl << std::endl;

return 0;
}
  • 编译和运行: 你可以使用像 g++ 这样的编译器来编译运行:

    Bash

    Code
    1
    2
    g++ your_file_name.cpp -o output_name -std=c++11
    ./output_name

    (注意: typeid().name() 输出的类型名称可能因编译器而异,例如 i 代表 int, d 代表 double。)


4. Q&A 闪卡 (Flash Cards)

问题 (Question) 答案 (Answer)
什么是 C++ 函数模板的默认模板参数? 在定义模板时,可以给模板参数指定一个默认类型。如果在调用函数时没有为该参数提供显式类型,编译器就会使用这个默认类型。
为什么默认模板参数很有用,特别是对于返回类型? 因为编译器通常无法根据函数调用参数自动推断出返回类型。设置默认返回类型后,如果不想用默认值,可以显式指定;如果想用默认值,则调用时可以省略,使代码更简洁。
如何为模板参数设置默认值? template <...> 声明中,使用 = 符号给参数赋值,例如 template <typename T = int, typename RetT = double>
如果我显式指定了模板参数,默认值还有效吗? 无效。显式指定的值会覆盖(优先于)默认值。
调用 maximum(10, 20.5) 时,如果 maximum 的定义是 template <typename RetT = double, typename T, typename P>RetT 会是什么类型? double。因为调用时没有显式指定 RetT,编译器会使用默认值 doubleT 会被推断为 intP 会被推断为 double
调用 minimum<int>(5.5, 3) 时,如果 minimum 定义是 template <typename T, typename P, typename RetT = double>RetT 是什么类型? double。这里显式指定了第一个模板参数 Tint。编译器根据调用参数推断 Pint。因为 RetT 没有被显式指定,所以使用其默认值 double。注意,传入的 5.5 会被转换为 int (值为5) 来匹配 T

5. 常见误解或错误

  1. 误以为所有模板参数都可以省略: 只有那些编译器能从函数调用参数中推断出来,或者具有默认值的模板参数才能省略。如果一个模板参数(如返回类型)既不能被推断,又没有默认值,那么在调用时必须显式指定它。
  2. 忘记显式指定会覆盖默认值: 如果你为某个有默认值的模板参数显式指定了一个不同的类型,那么将使用你指定的类型,而不是默认类型。
  3. 忽略窄化转换带来的精度损失: 当显式指定或默认的返回类型范围小于计算结果的类型时(例如,返回 int 但计算结果是 double),会发生窄化转换,可能导致小数部分丢失,结果并非预期。
  4. 默认参数顺序问题: 虽然C++11后规则有所放宽,但习惯上,最好将具有默认值的模板参数放在列表的末尾,或者确保所有没有默认值的参数都能被推断出来。将无法推断且没有默认值的参数放在有默认值的参数 之后 通常是错误的。
  5. 过度依赖默认值导致代码不清晰: 虽然默认参数很方便,但如果一个函数模板有多个默认参数,并且在特定调用中依赖了多个默认值,有时代码的可读性会下降。显式指定关键参数(尤其是返回类型,如果它与推断的类型不同)有时能让意图更明确。

6. 编码练习

现在,轮到你来实践了!下面有一个计算两个数之和的函数模板。请你:

  1. 为返回类型 RetT 添加一个默认模板参数,使其默认为 double
  2. 为第二个参数类型 P 也添加一个默认模板参数,使其默认为 int
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
50
#include <iostream>
#include <typeinfo>

// TODO: 修改下面的模板声明,为 RetT 添加 double 默认值,为 P 添加 int 默认值
template <typename RetT, typename T, typename P>
RetT add(T a, P b) {
std::cout << " [add 函数内部] a 类型: " << typeid(T).name()
<< ", b 类型: " << typeid(P).name() << std::endl;
RetT result = static_cast<RetT>(a) + static_cast<RetT>(b);
std::cout << " [add 函数内部] 计算并返回类型 RetT: " << typeid(RetT).name() << std::endl;
return result;
}

int main() {
double val1 = 10.5;
int val2 = 5;

std::cout << "--- 测试 add 函数 ---" << std::endl;

// 调用 1: 应该使用默认的 RetT(double) 和 P(int)
// T 会被推断为 double
std::cout << "1. 调用 add(" << val1 << ", " << val2 << ") - T=double, 依赖 P 和 RetT 的默认值" << std::endl;
// 这里编译器期望 P 是 int (默认),但我们传入了 val2 (int),类型匹配。
// 返回类型期望是 double (默认)。
auto result1 = add(val1, val2); // T 推断为 double, P 使用默认 int, RetT 使用默认 double
std::cout << " 结果: " << result1 << ", 类型: " << typeid(result1).name() << std::endl << std::endl;

// 调用 2: 显式指定 RetT 为 int,覆盖默认值
// P 仍然使用默认值 int
// T 会被推断为 double
std::cout << "2. 调用 add<int>(" << val1 << ", " << val2 << ") - T=double, P 默认 int, 显式 RetT=int" << std::endl;
auto result2 = add<int>(val1, val2); // T 推断为 double, P 默认 int, RetT 指定为 int
std::cout << " 结果: " << result2 << ", 类型: " << typeid(result2).name() << std::endl << std::endl;

// 调用 3: 显式指定 RetT 和 T,让 P 使用默认值
// 注意:模板参数是按顺序指定的 RetT, T, P
std::cout << "3. 调用 add<double, double>(" << val1 << ", " << val2 << ") - RetT=double, T=double, P 默认 int" << std::endl;
// 我们传入的 val2 是 int,但 P 的默认值也是 int,所以匹配。
auto result3 = add<double, double>(val1, val2); // RetT=double, T=double, P 默认 int
std::cout << " 结果: " << result3 << ", 类型: " << typeid(result3).name() << std::endl << std::endl;

// 调用 4: 显式指定所有参数,覆盖 P 的默认值
std::cout << "4. 调用 add<float, float, float>(" << val1 << ", " << val2 << ") - 指定所有类型为 float" << std::endl;
// 传入的 double 和 int 都会转换为 float
auto result4 = add<float, float, float>(val1, val2); // RetT=float, T=float, P=float
std::cout << " 结果: " << result4 << ", 类型: " << typeid(result4).name() << std::endl << std::endl;


return 0;
}

请你将上面代码中 // TODO: 下一行的 template <...> 部分修改正确,使得整个程序能够按预期编译和运行。完成后,请将你修改后的 template 声明行告诉我。

作业:

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename RetT = double, typename T = int, typename P>

RetT add(T a, P b) {

    std::cout << "  [add 函数内部] a 类型: " << typeid(T).name()

              << ", b 类型: " << typeid(P).name() << std::endl;

    RetT result = static_cast<RetT>(a) + static_cast<RetT>(b);

    std::cout << "  [add 函数内部] 计算并返回类型 RetT: " << typeid(RetT).name()

              << std::endl;

    return result;

}

非常棒!你尝试修改了模板声明,这是学习过程中非常重要的一步。让我们一起来看看你的答案,并分析一下。


7. 评分与解答

你的答案:

C++

Code
1
template <typename RetT = double, typename T = int, typename P>

评分: 3 / 5 ⭐⭐⭐

分析:

  • 做得好的地方: 你正确地为第一个模板参数 RetT 添加了 double 作为默认值 (RetT = double)。这完全符合要求!👍
  • 需要改进的地方: 题目要求是为 第二个参数类型 P 添加 int 默认值。但你的代码是为第二个模板参数 T 添加了 int 默认值 (T = int),而 P 仍然没有默认值。这是一个小小的偏差。

为什么你的代码可能仍然能编译运行?

C++ 的函数模板参数推导机制非常强大。在你提供的 main 函数调用中:

  • add(val1, val2): 编译器看到第一个参数 val1double,第二个参数 val2int。它会尝试推导 TP。它推导 Tdouble(这会覆盖你设置的 T=int 默认值),推导 PintRetT 因为没有显式指定,所以使用默认的 double。所以这次调用能成功。
  • 其他调用也类似,要么显式指定了类型,要么编译器能够成功推导出 TP,覆盖了你为 T 设置的默认值。

但是, 尽管它可能碰巧能工作,但它并没有完全按照指示来做(给 P 而不是 T 设置默认值)。而且,通常我们不会给那些总是能通过函数参数直接推导出来的类型(比如这里的 TP)设置默认值,除非有特殊需求。默认值对于像返回类型 RetT 这样无法自动推导的参数更有意义。

正确的答案 (严格按照指示修改原模板):

C++

Code
1
2
3
4
5
// 正确答案:为 RetT 添加 double 默认值,为 P 添加 int 默认值
template <typename RetT = double, typename T, typename P = int>
RetT add(T a, P b) {
// ... 函数体不变 ...
}
  • 这里 RetT 默认为 double
  • T 没有默认值,编译器会根据第一个函数参数 a 来推导它。
  • P 默认为 int。如果调用时第二个参数 b 的类型可以推导出 P,则使用推导出的类型;如果无法推导或省略(在更复杂的场景下),则会使用默认的 int

更好的实践 (符合默认参数通常放最后的风格):

虽然上面的答案严格符合了修改要求,但在 C++ 中,更常见的做法是将带有默认值的模板参数放在列表的末尾。所以,一个更符合工程实践风格的定义可能是(如果我们可以重新排序参数的话):

C++

Code
1
2
3
4
5
// 更好的实践:将带默认值的参数放在后面
template <typename T, typename P = int, typename RetT = double>
RetT add(T a, P b) {
// ... 函数体不变 ...
}

在这个版本中,TP 会优先尝试从函数参数 ab 推导,如果 P 由于某种原因无法推导(或者在模板特化等场景下),它会是 intRetT 如果不显式指定,则默认为 double


8. 扩展思考与探索

做得很好!现在你已经了解了函数模板的默认参数,这里有一些更进一步的问题,可以帮助你深化理解:

  1. 混合使用默认参数和显式参数:
    • 如果模板定义是 template <typename T = int, typename U = float, typename V = double>
    • 当你调用 func<long>(/*...args...*/) 时,T, U, V 分别是什么类型?
    • 当你调用 func<char, long>(/*...args...*/) 时,T, U, V 又分别是什么类型?
  2. 类模板 (Class Template) 的默认参数: 默认模板参数不仅可以用在函数模板上,也可以用在类模板上。你能否尝试定义一个简单的类模板 MyContainer,让它有一个类型参数 T 代表容器存储的元素类型,并让 T 默认是 int?然后实例化这个模板,一次使用默认类型,一次指定为 double
  3. 非类型模板参数 (Non-type Template Parameters) 的默认值: 模板参数不一定总是类型 (typenameclass),它也可以是具体的常量值,比如整数或指针。例如 template <int Size = 10>。这种非类型模板参数也可以有默认值吗?你能写一个简单的例子吗?
  4. 默认参数与模板特化 (Template Specialization) 的关系: 如果你为一个带有默认参数的模板定义了特化版本,默认参数规则是如何应用的?
  5. C++11 及以后的改进: C++ 标准在不断发展。了解一下从 C++11 开始,对于模板默认参数的规则是否有任何变化或放宽?(提示:关于默认参数不必严格放在最后的规则有所放宽,但前提是后续参数可以被推导)。

不要害怕尝试和犯错,动手编写代码是最好的学习方式!继续探索,你会发现 C++ 模板的强大和灵活之处。如果你有任何问题,随时都可以再来问我!


评论