avatar

目录
260 - Named Template Parameters for Lambdas

260 - Named Template Parameters for Lambdas

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

好的,新手同学你好!很高兴能作为你的 C++ 启蒙老师。C++ 功能强大但也有些复杂,别担心,我会用最清晰、最循序渐进的方式带你入门。今天我们要学习 C++20 带来的一个很酷的特性:Lambda 表达式的具名模板参数。


1. 标题与摘要

标题: C++20 新特性:为 Lambda 函数(Lambda functions)指定具名模板参数

摘要:

本节课我们将学习 C++ Lambda 表达式的一个高级用法。通常,当你使用 auto 关键字作为 Lambda 参数类型时,编译器(compiler)会自动将其变成一个模板(template)。但有时我们希望更精确地控制参数类型,例如强制要求所有参数必须是同一类型。C++20 引入了新语法,允许我们为 Lambda 显式命名模板参数,从而实现这种精确控制。


2. 详细解释

让我们一步步来理解这个概念:

  • 回顾:使用 auto 的 Lambda 函数

    • 在之前的 C++ 版本(以及 C++20 中),你可以这样写 Lambda:

      C++

      Code
      1
      2
      3
      auto add = [](auto a, auto b) {
      return a + b;
      };
    • 这里的 auto aauto b 告诉编译器:“请接受任何类型的参数 ab,并尝试将它们相加”。

    • 当你这么做时,编译器实际上在“幕后”为你创建了一个函数模板。这意味着你可以用不同类型的参数调用它:

      C++

      Code
      1
      2
      3
      add(5, 10);      // a 是 int, b 是 int, 返回 int
      add(3.14, 2.71); // a 是 double, b 是 double, 返回 double
      add(5, 6.5); // a 是 int, b 是 double, 返回 double (类型提升)
    • 这种自动模板化的方式很方便,但有时会带来问题。

  • 问题:缺乏类型约束

    • 上面的 add(5, 6.5) 例子中,参数类型不同(intdouble)。相加的结果类型会根据 C++ 的隐式类型转换规则(通常是范围更大的类型,这里是 double)来确定。
    • 想象一下,如果你的 Lambda 函数体内的逻辑很复杂,涉及很多不同类型的变量,那么最终的返回类型推导(return type deduction)可能会变得难以预测和管理。
    • 有时候,你可能明确希望你的 Lambda 只处理相同类型的参数。例如,你可能只想对两个整数相加,或者两个浮点数相加,但不允许混合类型。在 C++20 之前,要对 Lambda 实现这种约束比较麻烦。
  • C++20 的解决方案:具名模板参数

    • C++20 引入了一种简洁的语法,让你可以在 Lambda 中显式声明和命名模板参数。
    • 语法结构如下:

      C++

      Code
      1
      2
      3
      4
      auto lambda_name = [] <typename T /*, typename P, ... */> (T param1, T param2 /*, P param3, ...*/) {
      // Lambda body
      return param1 + param2 /* + ... */;
      };
    • 关键点解析:

      1. []: 这是 Lambda 的捕获列表(capture list),我们暂时不关注它,保持为空。
      2. <typename T>: 这就是新加的部分!它位于捕获列表 [] 和参数列表 () 之间,用尖括号 <> 包裹。
        • typename (或者 class) 是关键字,表示我们要声明一个类型参数(type parameter)。
        • T 是我们给这个类型参数起的名字(你可以用任何合法的标识符,比如 MyType,但 T, U, P 是常用约定)。
        • 你可以声明多个模板参数,用逗号隔开,例如 <typename T, typename P>
      3. (T a, T b): 这是 Lambda 的函数参数列表(function parameter list)。现在,我们不再使用 auto,而是直接使用我们刚刚在尖括号里声明的类型参数 T 来指定 ab 的类型。
    • 效果:强制类型一致

      • 通过写 (T a, T b),我们告诉编译器:“这个 Lambda 接受两个参数,并且这两个参数的类型必须是同一个类型 T”。T 具体是什么类型,将在调用 Lambda 时由编译器根据传入的实参推断出来。
      • 现在,如果我们尝试用不同类型的参数调用它:

        C++

        Code
        1
        2
        3
        4
        5
        6
        7
        auto add_same_type = [] <typename T> (T a, T b) {
        return a + b;
        };

        add_same_type(5, 10); // OK! T 被推断为 int
        add_same_type(3.14, 2.71); // OK! T 被推断为 double
        // add_same_type(5, 6.5); // 编译错误 (Compiler Error)! 无法将 T 同时推断为 int 和 double
      • 这就实现了我们之前想要的类型约束!

  • 使用多个模板参数

    • 如果你需要接受不同类型的参数,但仍想明确控制它们,可以声明多个模板参数:

      C++

      Code
      1
      2
      3
      4
      5
      6
      7
      8
      auto process_different = [] <typename T, typename P> (T first, P second) {
      // 可以对不同类型的 first 和 second 进行操作
      std::cout << "First (Type T): " << first << ", Second (Type P): " << second << std::endl;
      // 返回类型仍然可以用 auto 推导,或者显式指定
      };

      process_different(10, "Hello"); // OK! T 推断为 int, P 推断为 const char*
      process_different(3.14, 'A'); // OK! T 推断为 double, P 推断为 char
    • 这种方式比简单的 (auto a, auto b) 提供了更强的类型控制感和代码可读性。

  • 返回类型推导

    • 即使使用了具名模板参数,Lambda 的返回类型通常仍然可以通过 return 语句自动推导,就像以前使用 auto 参数时一样。编译器会根据 Lambda 函数体内的 return 表达式来决定返回类型。如果你想显式指定返回类型,也可以使用 -> ReturnType 语法。
  • 与类模板的关系(进阶提示)

    • 原文提到,完全理解其工作原理需要学习 C++ 的自定义类型(类)。简单来说,编译器处理 Lambda 时,实际上会生成一个匿名的类(或称为函数对象 Functor),而带 auto 或具名模板参数的 Lambda 会生成一个类模板。这个知识点你以后学习类和模板时会更清晰,现在只需掌握如何使用这个语法即可。

3. 代码示例

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
#include <iostream>
#include <string>
#include <vector> // 包含 vector 头文件

// C++20 需要在编译时启用,例如 g++ 使用 -std=c++20

// 示例 1: 使用 auto 的隐式模板 Lambda
auto add_auto = [](auto a, auto b) {
return a + b;
};

// 示例 2: 使用 C++20 具名模板参数强制同类型
auto add_same_type = [] <typename T> (T a, T b) {
// 现在 a 和 b 必须是相同类型 T
return a + b;
};

// 示例 3: 使用多个具名模板参数
auto combine_different = [] <typename T1, typename T2> (const T1& item1, const T2& item2) {
// 这里用 const& 避免不必要的拷贝,特别是对于复杂类型
std::cout << "Combining: " << item1 << " and " << item2 << std::endl;
// 返回类型可以自动推导,这里我们不返回值 (void)
};

int main() {
std::cout << "--- 示例 1: auto Lambda ---" << std::endl;
std::cout << "add_auto(5, 10) = " << add_auto(5, 10) << std::endl; // int + int -> int
std::cout << "add_auto(3.5, 1.2) = " << add_auto(3.5, 1.2) << std::endl; // double + double -> double
std::cout << "add_auto(5, 3.5) = " << add_auto(5, 3.5) << std::endl; // int + double -> double
std::string s1 = "Hello", s2 = " C++";
std::cout << "add_auto(s1, s2) = " << add_auto(s1, s2) << std::endl; // string + string -> string

std::cout << "\n--- 示例 2: 强制同类型 Lambda ---" << std::endl;
std::cout << "add_same_type(5, 10) = " << add_same_type(5, 10) << std::endl; // OK, T = int
std::cout << "add_same_type(3.5, 1.2) = " << add_same_type(3.5, 1.2) << std::endl; // OK, T = double
std::cout << "add_same_type(s1, s2) = " << add_same_type(s1, s2) << std::endl; // OK, T = std::string

// 下面这行如果取消注释,会导致编译错误,因为参数类型不同
// std::cout << "add_same_type(5, 3.5) = " << add_same_type(5, 3.5) << std::endl;
std::cout << "(调用 add_same_type(5, 3.5) 会导致编译错误)" << std::endl;

std::cout << "\n--- 示例 3: 多模板参数 Lambda ---" << std::endl;
combine_different(100, "Apples"); // OK, T1=int, T2=const char*
combine_different(std::string("Vector size"), std::vector<int>{1, 2, 3}.size()); // OK, T1=std::string, T2=size_t (vector::size() 返回类型)
combine_different(3.14f, 99L); // OK, T1=float, T2=long

return 0;
}

编译提示: 编译这个代码需要支持 C++20 的编译器,并启用 C++20 标准。例如使用 g++:

g++ your_code.cpp -o output -std=c++20

或使用 Clang++:

clang++ your_code.cpp -o output -std=c++20


4. QA 闪卡 (Flashcards)

  • Q1: C++20 中,在 Lambda 的哪个位置可以添加具名模板参数声明?

    A1: 在捕获列表 [] 和参数列表 () 之间,使用尖括号 <…>。例如: [] (…)。

  • Q2: [](T a, T b) 这样的 Lambda 语法主要解决了什么问题?

    A2: 它解决了需要强制 Lambda 的两个(或多个)参数必须是相同类型的问题,提供了比 (auto a, auto b) 更强的类型约束。

  • Q3: 使用 auto 作为 Lambda 参数 (例如 {…}) 和使用 [](T a){…} 的根本区别是什么?

    A3: 两者都使 Lambda 成为模板。但 auto 是隐式的,每个 auto 参数都可能代表不同的类型。 是显式的,你可以用同一个名字 T 来约束多个参数必须是相同类型。

  • Q4: 在 [] (T x, P y) 中, T 和 P 可以是相同类型吗?

    A4: 可以。编译器会根据调用时传入的参数独立推断 T 和 P。如果传入两个 int,那么 T 和 P 都会被推断为 int。


5. 常见误解与易犯错误

  1. 语法位置错误: 最常见的错误是把 <typename T> 放错位置。

    • 错误: [] (T a, T b) <typename T> {...}
    • 错误: <typename T> [] (T a, T b) {...}
    • 正确: [] <typename T> (T a, T b) {...} (记住:紧跟在 [] 之后,() 之前)
  2. 忘记 typenameclass 关键字: 在尖括号里必须使用 typenameclass 来声明类型参数。

    • 错误: [] <T> (T a, T b) {...}
    • 正确: [] <typename T> (T a, T b) {...}[] <class T> (T a, T b) {...} (两者在这里等价)
  3. 误以为返回类型也必须是 T 除非你在 return 语句中明确返回类型 T 的变量,否则返回类型仍然由 return 表达式本身决定。

    • 例如:[] <typename T> (T a, T b) { return 1.0; } 这个 Lambda 总是返回 double,不管 T 被推断成什么。
    • [] <typename T> (T a, T b) { return a; } 的返回类型将与 T 相同。
    • [] <typename T> (T a, T b) { return a + b; } 的返回类型取决于 T 类型相加的结果类型(例如,如果 Tshorta+b 可能会提升为 int 并返回 int)。
  4. 与模板函数混淆: 虽然效果类似,但这是针对 Lambda 的特定语法,让你不必将 Lambda 包装在一个单独的模板函数中就能获得模板化的能力和约束。

6. 编码练习

现在轮到你来试试了!请填充下面的代码片段中的 /* ??? */ 部分,使其能够正确工作。目标是创建一个名为 print_if_same_type 的 Lambda 函数,它接受两个参数。只有当两个参数的类型完全相同时,它才会打印这两个参数;否则,它什么也不做。你需要使用 C++20 的具名模板参数来确保只有同类型才能匹配这个 Lambda。

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
#include <iostream>
#include <string>
#include <typeinfo> // 用于 typeid

int main() {
// TODO: 创建一个 Lambda 'print_if_same_type'
// 它接受两个相同类型的参数。
// 使用 C++20 具名模板参数语法来约束这两个参数必须类型相同。
// 在 Lambda 体内,打印出这两个参数的值。
// (提示:Lambda 不需要返回值,可以是 void)
auto print_if_same_type = [] /* ??? */ < /* ??? */ > ( /* ??? */ val1, /* ??? */ val2) /* -> void (可选) */ {
std::cout << "Same type detected (" << typeid(T).name() << "): "
<< val1 << ", " << val2 << std::endl;
};

int i1 = 10, i2 = 20;
double d1 = 3.14;
std::string s1 = "Hello", s2 = "World";

std::cout << "--- Testing print_if_same_type ---" << std::endl;
print_if_same_type(i1, i2); // 应该打印
print_if_same_type(s1, s2); // 应该打印
// print_if_same_type(i1, d1); // 调用这个函数应该导致编译时找不到匹配的模板实例(即编译失败)
// print_if_same_type(s1, i1); // 同上

std::cout << "(Calls with different types would fail to compile)" << std::endl;

return 0;
}

请将你的答案写出来,我会帮你检查。加油!

homework:

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
#include <iostream>
#include <string>
#include <typeinfo> // 用于 typeid

int main() {
// 正确的 Lambda 定义
auto print_if_same_type = [] <typename T> (T val1, T val2) {
// 注意:这里 val1 和 val2 都使用同一个类型参数 T
std::cout << "Same type detected (" << typeid(T).name() << "): "
<< val1 << ", " << val2 << std::endl;
};

int i1 = 10, i2 = 20;
double d1 = 3.14;
std::string s1 = "Hello", s2 = "World";

std::cout << "--- Testing print_if_same_type ---" << std::endl;
print_if_same_type(i1, i2); // OK, T 推断为 int
print_if_same_type(s1, s2); // OK, T 推断为 std::string

// 现在,如果取消下面这行的注释,会导致编译错误
// print_if_same_type(i1, d1); // 错误:无法推断出唯一的 T (int vs double)

std::cout << "(Calls with different types would fail to compile)" << std::endl;

return 0;
}


评论