Lecture 11 (Week 6 - Tuesday): 函数与 Lambda (Functions & Lambdas)

目录 · ← l10 · l12 →

Lecture 11 (Week 6 - Tuesday): 函数与 Lambda (Functions & Lambdas)

概述

本讲解决一个根本问题:如何把”行为”当作值来传递。课程从布尔值函数——谓词(predicate)——出发,先看函数指针(bool(*)(char))这种朴素方案,再用 lambda 表达式与捕获子句(capture clause)解决”函数需要携带状态”的痛点,并揭示 lambda 的真相:它会被编译器脱糖(desugar)成一个匿名的 functor 类。随后系统讲解 <algorithm> 的标准算法(find_if/count_if/sort/transform/copy_if/unique_copy),以课堂 tokenizer 为例演示”把问题识别成标准算法”的方法论;最后引入 C++20 的 ranges 与 views——惰性、可组合的算法流水线,与 Python 生成器逐行类比。本讲内容直接覆盖 A4 的前半部分(课堂会在课上带写第一段代码)。

核心特性与语法详解

1. 谓词(Predicate)

  • 定义与目的:谓词是返回 bool 的函数。它把”判断”抽象成一个可传递的值,让算法问出任意问题(”是不是元音?”“是否可被 5 整除?”),而不是只做等值比较。
  • 核心语法
    bool isVowel(char c) { ... }          // 一元谓词:接收 1 个元素
    bool isDivisible(int n, int d) { ... } // 二元谓词:接收 2 个元素
    
  • 设计意图与最佳实践std::find 只能找”等于某个值”的元素;把谓词作为参数(如 std::find_if),同一个算法就能回答任意问题。这是”用用户定义的行为泛化算法”的关键一步。

2. 函数指针

  • 定义与目的:函数在 C++ 中也有地址,可以存进指针变量再传给算法。find_if(begin, end, isVowel) 中模板参数 Pred 被推导为 bool(*)(char)
  • 核心语法bool (*fp)(char) = isVowel;——fp 是指向”接收 char、返回 bool 函数”的指针。
  • 设计意图与最佳实践:函数指针无法携带状态——想找”小于 N 的数”而 N 要运行时才知道?函数指针只能指向”小于 5”“小于 6”“小于 7”这种写死的函数,无法把 N 带进去。这是它”泛化能力差”的根源,引出 lambda。

3. Lambda 与捕获子句

  • 定义与目的:lambda 是从外层作用域捕获状态的函数,本质是就地定义的匿名 functor。它解决了函数指针”无状态”的缺陷:[n](int x) { return x < n; } 把运行时读到的 n 装进函数里。
  • 核心语法
    auto lessThanN = [n](int x) { return x < n; };   // 捕获子句 [n] + 参数 + 函数体
    [x]      // 按值捕获 x(复制一份)
    [x&]     // 按引用捕获 x
    [x, y]   // 按值捕获 x、y
    [&]      // 全部按引用捕获
    [&, x]   // 除 x 按值外,其余按引用
    [=]      // 全部按值捕获
    
  • 设计意图与最佳实践:捕获子句里的变量在 lambda 体内可见(普通函数体内只有参数可见)。auto 参数(泛型 lambda,C++14)是模板的简写:[](auto x) { ... } 等价于 template <typename T> ...,编译器在调用点隐式实例化。不需要捕获时可以不写捕获子句,lambda 就退化成普通匿名函数,还能隐式转换为函数指针。

4. Functor(函数对象)

  • 定义与目的:functor 是任何定义了 operator() 的对象——”行为像函数的对象”。因为它是对象,所以可以拥有成员变量(状态)。
  • 核心语法
    struct my_functor {
      int operator()(int a) const { return a * value; }   // 函数调用运算符
      int value;                                          // 状态!
    };
    my_functor f; f.value = 5; f(10);                     // 50
    
  • 设计意图与最佳实践:STL 里 std::greater<T>std::hash<T> 都是 functor。lambda 的底层实现就是一个编译器生成的匿名 functor 类(详见示例 3)。std::function 则是一个”包罗万象”的类型擦除容器:任何函数指针、lambda、functor 都可以转换进去(代价是稍慢),日常更推荐 auto + 模板,不显式操心类型。

5. STL 算法(<algorithm>

  • 定义与目的<algorithm>模板函数的集合,全部基于迭代器操作,覆盖排序、查找、计数、变换、复制、去重等。STL 四大件:容器(存什么)、迭代器(怎么遍历)、算法(怎么通用地处理)、functor(怎么表示行为)。
  • 核心语法
    std::count_if(first, last, p);      // [first, last) 中满足 p 的元素个数
    std::sort(first, last, comp);       // 按 comp 排序
    std::max_element(first, last, comp);// 按 comp 找最大元素
    std::copy_if(r1, r2, o, p);         // 把满足 p 的元素复制到输出 o
    std::transform(r1, r2, o, op);      // 对每个元素应用 op,写入 o
    std::unique_copy(i1, i2, o, p);     // 去掉连续重复,写入 o
    
  • 设计意图与最佳实践:方法论(课堂 tokenizer 三步骤):①先手工演算一个例子;②提炼逻辑;③判断它是否对应某个标准算法——是,就用标准库,换取正确性与可读性。

6. Ranges 与 Views(C++20/23)

  • 定义与目的range 是任何”有 beginend“的东西(容器、自定义类型);view惰性地适配另一个 range 的 range——不复制元素,边遍历边按需计算。ranges 是 STL v2:新的范围算法(std::ranges::find(v, 'c') 直接传容器)用 concepts 约束,报错更好;views 让我们用 \| 管道把算法组合成流水线。
  • 核心语法
    auto it = std::ranges::find(v, 'c');                        // 传整个容器
    auto view = letters \| std::ranges::views::filter(isVowel)
                        \| std::ranges::views::transform(toupper);
    std::vector<char> out = std::ranges::to<std::vector<char>>(view);  // 物化(C++23)
    
  • 设计意图与最佳实践范围算法是急切的std::ranges::sort(v) 立刻排序),views 是惰性的(构建流水线一行都不算,直到被消费/物化才逐元素求值)。views 就像 Python 生成器:(l for l in letters if isVowel(l))list(view) 物化。注意 std::ranges::to 是 C++23 特性,C++20 下用容器的迭代器构造函数物化(如 std::vector<char> out(view.begin(), view.end());)。

代码示例与逐步解说(核心)

示例 1:谓词 + 函数指针——find_if 与”无状态”困境(C++17)

代码

#include <algorithm>
#include <cctype>
#include <iostream>
#include <string>
#include <vector>

// 一元谓词:判断字符是否为元音
bool isVowel(char c) {
  c = std::toupper(c);
  return c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U';
}

// 另一个"行为":判断整数能否被 5 整除
bool isGood(int x) { return x % 5 == 0; }

int main() {
  std::string flower = "rose";
  auto it = std::find_if(flower.begin(), flower.end(), isVowel);
  if (it != flower.end()) *it = 'i';          // 找到 'o' 改成 'i' → "rise"

  std::vector<int> nums { 3, 7, 10, 12 };
  auto it2 = std::find_if(nums.begin(), nums.end(), isGood);   // 指向 10

  std::cout << flower << " " << *it2 << "\n";   // rise 10

  // 函数指针的真实类型:Pred = bool(*)(char)
  bool (*fp)(char) = isVowel;

  // 困境:想找"小于 N"的数,而 N 运行时才知道——
  // 函数指针指向写死的函数,无法携带 N 这个状态!
  // find_if(begin, end, ???)   // 没有现成函数可用
}

代码做什么:把 isVowelisGood 两个谓词直接传给 std::find_if,分别找出字符串中的第一个元音和数组里第一个能被 5 整除的数,并展示函数指针的类型写法。

特性机制解说std::find_if 的签名是 template <typename InputIt, typename UnaryPred> InputIt find_if(InputIt first, InputIt last, UnaryPred p);——UnaryPred 由实参推导:第一次调用 Pred = bool(*)(char),第二次 Pred = bool(*)(int)。算法内部对每个元素执行 p(*it),因此用户定义的行为被注入到通用算法中。注意 find_iffind 的差别:find 问”等于 value 吗?”,find_if 问”满足谓词吗?”——后者能回答任意问题。函数指针语法 bool (*fp)(char) 要拆开读:fp 先与 * 结合是指针,bool(...)(char) 说明它指向”接收 char、返回 bool”的函数。

示例 2:Lambda 捕获与捕获子句大全(C++17/20)

代码

#include <algorithm>
#include <functional>
#include <iostream>
#include <string>
#include <vector>

int main() {
  std::vector<int> v { 3, 1, 4, 1, 5, 9, 2, 6 };

  int n = 5;
  // [n]:按值捕获 n(复制一份);x 是参数
  auto lessThanN = [n](int x) { return x < n; };
  auto it = std::find_if(v.begin(), v.end(), lessThanN);
  std::cout << "第一个 < n 的元素: " << *it << "\n";          // 3

  int count = std::count_if(v.begin(), v.end(), [n](int x) { return x < n; });
  std::cout << "小于 n 的元素个数: " << count << "\n";        // 5

  // 捕获子句一览
  int a = 1, b = 2;
  auto f1 = [a, b](int x) { return x + a + b; };  // 按值捕获 a、b
  auto f2 = [&](int x) { return x + a + b; };     // 全部按引用捕获
  auto f3 = [&, a](int x) { return x + a + b; };  // 全部按引用,但 a 按值
  auto f4 = [=, &b](int x) { return x + a + b; }; // 全部按值,但 b 按引用

  // 无捕获 lambda 可转换为函数指针
  int (*fp)(int) = [](int x) { return x * 2; };
  std::cout << fp(21) << "\n";                    // 42

  // auto 参数 = 泛型 lambda(C++14):等价于模板
  auto generic = [](auto x, auto y) { return x + y; };
  std::cout << generic(1, 2) << " "
            << generic(std::string("a"), std::string("b")) << "\n";  // 3 ab

  // 排序:默认升序 vs 自定义比较
  std::sort(v.begin(), v.end(), [](int x, int y) { return x > y; });  // 降序
  std::cout << v.front() << "\n";                 // 9

  // std::function:函数指针/lambda 的统一容器(稍慢)
  std::function<bool(int)> pred = lessThanN;
  std::cout << pred(3) << "\n";                   // 1 (true)
}

代码做什么:用 lambda 完成”小于 N”的查找与计数(N 运行时才知道——函数指针做不到),遍历捕获子句的各种写法,并演示泛型 lambda、函数指针转换、自定义排序与 std::function

特性机制解说:lambda 语法 [捕获](参数) { 函数体 } 中,捕获子句决定外层变量如何进入函数体:[n] 复制一份(快照),[&n] 持有引用(能看到后续修改)。函数体里只有参数和捕获变量在作用域内。[](auto x)auto 参数让 lambda 变成泛型 lambda——编译器把它翻译成一个 template <typename T>operator(),与普通模板函数同理(示例里同一 lambda 既用于 int 又用于 std::string)。无捕获 lambda 因为没有状态,可以直接退化为函数指针。std::sort 的第三参数是二元谓词(比较器),默认 operator< 升序,传 x > y 即降序。std::function<bool(int)> 是类型擦除的”函数容器”:lambda、函数指针、functor 都能放进去,代价是间接调用稍慢,日常用 auto 即可。

示例 3:Lambda 的真相——脱糖为匿名 functor 类(C++17)

代码

#include <algorithm>
#include <iostream>
#include <vector>

// 编译器眼中的 lambda:[n](int x){ return x < n; }
// 会被展开成这样一个匿名的 functor 类(名字是编译器编的,如 lambda_6_18):
class lambda_6_18 {
public:
  bool operator()(int x) const { return x < n; }   // 函数调用运算符
  lambda_6_18(int _n) : n{_n} {}                   // 捕获变量经构造参数传入
private:
  int n;                                           // 捕获的变量变成成员字段
};

int main() {
  int n = 10;
  std::vector<int> v { 3, 1, 4, 1, 5, 9, 2, 6 };

  // 写法 A:lambda 语法糖
  auto lessThanN = [n](int x) { return x < n; };
  auto itA = std::find_if(v.begin(), v.end(), lessThanN);

  // 写法 B:手写 functor 类——与 A 完全等价
  lambda_6_18 lessThanN2(n);
  auto itB = std::find_if(v.begin(), v.end(), lessThanN2);

  std::cout << *itA << " " << *itB << "\n";        // 3 3
}

代码做什么:同一份逻辑分别用 lambda 与手写 functor 类实现,二者在 find_if 中行为完全一致,验证”lambda 就是 functor 的语法糖”。

特性机制解说:lambda 的脱糖规则(课堂幻灯片原版):①捕获的变量变成类的成员字段(按值捕获 → 值成员,按引用捕获 → 引用成员);②函数体变成 operator()(参数) const;③捕获变量经构造函数从外层传入。于是 [n](int x) { return x < n; } 生成 lambda_6_18 { n }find_if 拿到的是这个对象。这也解释了为什么 lambda 有独特的不可拷贝/不可命名类型——auto 是唯一方便的写法(”我不知道类型,但编译器知道”)。functor 因为本质是对象,天然拥有状态——课堂 my_functor 例子用成员 value 充当乘法因子。这与范围 for 循环脱糖成迭代器循环是同一类”语法糖”现象;想亲眼看到展开过程可以试试 cppinsights.io。顺带一提:std::hash<T> 等 STL functor 也是这么工作的(模板特化 + operator())。

示例 4:传统 STL 算法管线——Soundex 经典版(C++17,课堂代码改写)

代码

#include <algorithm>
#include <cctype>
#include <iostream>
#include <iterator>
#include <map>
#include <string>

// 把字母映射成 Soundex 编码数字(课堂 soundex.cpp 的编码表)
static char soundexEncode(char c) {
  static const std::map<char, char> encoding = {
    {'A','0'},{'E','0'},{'I','0'},{'O','0'},{'U','0'},{'H','0'},{'W','0'},{'Y','0'},
    {'B','1'},{'F','1'},{'P','1'},{'V','1'},
    {'C','2'},{'G','2'},{'J','2'},{'K','2'},{'Q','2'},{'S','2'},{'X','2'},{'Z','2'},
    {'D','3'},{'T','3'},{'L','4'},{'M','5'},{'N','5'},{'R','6'}
  };
  return encoding.at(std::toupper(c));
}

static bool notZero(char c) { return c != '0'; }

std::string soundex(const std::string& s) {
  // 1) 只保留字母
  std::string letters;
  std::copy_if(s.begin(), s.end(), std::back_inserter(letters), ::isalpha);

  char first_letter = letters[0];                            // 记住首字母

  // 2) 每个字母 → 编码数字
  std::transform(letters.begin(), letters.end(), letters.begin(), soundexEncode);

  // 3) 去掉连续重复的编码
  std::string unique;
  std::unique_copy(letters.begin(), letters.end(), std::back_inserter(unique));

  unique[0] = std::toupper(first_letter);                    // 首字母放回

  // 4) 去掉 0 编码(元音等)
  std::string no_zeros;
  std::copy_if(unique.begin(), unique.end(), std::back_inserter(no_zeros), notZero);

  // 5) 补零到至少 4 位
  no_zeros += "0000";
  return no_zeros.substr(0, 4);
}

int main() {
  std::cout << soundex("Robert") << " " << soundex("Rupert") << "\n";  // R163 R163
}

代码做什么:完整实现经典 Soundex 算法(发音相似的名字得到相同编码):过滤字母 → 编码 → 去连续重复 → 还原首字母 → 去零 → 补零截断。RobertRupert 都得到 R163

特性机制解说:这是”识别标准算法”方法论的典范,每一行都是管道的一级:①copy_if + ::isalpha(谓词)把非字母滤掉,std::back_inserter(letters) 是输出迭代器,每写一个元素就 push_back 一个,免去手动扩容;②transform 把每个字母经 soundexEncode 变成数字字符,输入输出都是 letters(原地变换);③unique_copy 只消除连续重复(如 “LL”→”L”),这正是 Soundex 的”合并相邻同音”规则;④再一次 copy_ifnotZero 谓词去掉 ‘0’(元音/H/W/Y 编码);⑤字符串拼接 "0000"substr(0, 4) 保证定长 4。注意这套传统写法每一步都急切地物化一个完整容器lettersuniqueno_zeros 三个中间 std::string)——与下一节的 view 惰性管线形成鲜明对比。std::transform 还有二元重载:课堂 tokenizer 用 std::transform(spaces.begin(), spaces.end()-1, spaces.begin()+1, std::back_inserter(tokens), binary_op) 把”相邻两个空白迭代器”配对成 Token(这正是 A4 的做法)。

示例 5:Ranges 基础——惰性视图流水线(C++20/23)

代码

#include <algorithm>
#include <cctype>
#include <iostream>
#include <ranges>
#include <string>
#include <vector>

bool isVowel(char c) {
  c = std::toupper(c);
  return c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U';
}

int main() {
  std::vector<char> letters { 'a', 'b', 'c', 'd', 'e' };

  // 范围算法:直接传容器(容器是 range——有 begin/end)
  auto it = std::ranges::find(letters, 'c');
  std::cout << *it << "\n";                                  // c

  // 视图流水线:惰性、可组合(C++20 起可用;std::ranges::to 需 C++23)
  auto view = letters
    | std::ranges::views::filter(isVowel)                    // 只留元音
    | std::ranges::views::transform([](char c) { return std::toupper(c); });

  // 到这一行为止,什么也没算!view 只是一份"配方"
  // 注:std::ranges::to 是 C++23 特性(需 GCC 13+ / Clang 17+);本机 GCC 12 请用下方 C++20 写法
  std::vector<char> upperVowel = std::ranges::to<std::vector<char>>(view);  // C++23
  for (char c : upperVowel) std::cout << c;                  // AE
  std::cout << "\n";

  // C++20 兼容的物化方式:用迭代器构造容器
  auto view2 = letters | std::ranges::views::filter(isVowel);
  std::vector<char> v2(view2.begin(), view2.end());
  std::cout << v2.size() << "\n";                            // 2
}

代码做什么:先用 std::ranges::find 传整个容器查找,再用 \| 管道把 filter(只留元音)与 transform(转大写)组合成视图,最后物化为 vector<char>(’A’、’E’)。

特性机制解说:ranges 的核心概念:range = 有 begin/end 的东西std::ranges::range concept 即检查 ranges::begin(t)/ranges::end(t) 是否合法);范围算法(std::ranges::findsort 等)是 <algorithm> 的”重皮肤”版本,用 concepts 约束(template <ranges::input_range R, ...>),报错信息比迭代器版更友好。view 是惰性适配器filter 不复制元素、不提前过滤,只在被遍历到某个元素时才判断;transform 只在元素被取出时才调用函数。所以示例里 view 构造完时零计算,直到 std::ranges::to(C++23)或容器构造函数(C++20)开始消费它,才逐元素”拉取”数据。这就是流水线组合(\|)的意义:letters \| filter \| transform \| to 读起来像声明式的数据处理配方。对比 Python 生成器:

view = (l for l in letters if isVowel(l))     # 惰性
view = (l.upper() for l in view)              # 惰性
upperVowel = list(view)                       # 物化

filter 的两种调用形态等价:std::ranges::views::filter(letters, isVowel)letters \| std::ranges::views::filter(isVowel)(后者是”范围适配器闭包”,可链式拼接)。注意急切与惰性的区分std::ranges::sort(v) 是急切的(立刻真的排序),views 才是惰性的。

示例 6:Ranges 进阶——Soundex 的 ranges 版(C++23,课堂代码改写)

代码

#include <algorithm>
#include <cctype>
#include <iostream>
#include <map>
#include <ranges>
#include <string>

// Soundex 编码表:字母 -> 数字字符(与示例 4 的 soundexEncode 相同)
static char soundexEncode(char c) {
  static const std::map<char, char> encoding = {
    {'A','0'},{'E','0'},{'I','0'},{'O','0'},{'U','0'},{'H','0'},{'W','0'},{'Y','0'},
    {'B','1'},{'F','1'},{'P','1'},{'V','1'},
    {'C','2'},{'G','2'},{'J','2'},{'K','2'},{'Q','2'},{'S','2'},{'X','2'},{'Z','2'},
    {'D','3'},{'T','3'},
    {'L','4'},
    {'M','5'},{'N','5'},
    {'R','6'}
  };
  return encoding.at(std::toupper(c));
}
static bool notZero(char c) { return c != '0'; }

std::string soundexRanges(const std::string& s) {
  namespace rv = std::ranges::views;

  // 第一个字母:范围版 find_if 直接作用于字符串
  auto first = *std::ranges::find_if(s, ::isalpha);

  // 惰性管线:过滤字母 → 编码
  auto v = s | rv::filter(::isalpha) | rv::transform(soundexEncode);

  // 物化 + 去连续重复(范围版 unique_copy)
  std::string encoded;
  std::ranges::unique_copy(v, std::back_inserter(encoded));
  encoded[0] = std::toupper(first);          // 首字母放回

  return encoded
       | rv::filter(notZero)                 // 去掉 '0'
       | rv::take(4)                         // 只取前 4 个字符
       | std::ranges::to<std::string>();     // 物化成 string(C++23)
}

int main() {
  std::cout << soundexRanges("Robert") << "\n";   // R163
}

代码做什么:用 ranges 重写 Soundex:find_if 取首字母,filter \| transform 组成编码管线,unique_copy 去连续重复,最后 filter(notZero) \| take(4) \| to<string>() 产出定长 4 的编码。

特性机制解说:这个版本与示例 4 逻辑完全等价,但结构上是嵌套的惰性视图s \| rv::filter(::isalpha) \| rv::transform(soundexEncode) 构建了一个两层的适配器链——遍历 v 时,transformfilter 要元素,filter 再向 s 要字符并判断是否为字母,整条链只对实际取出的字符执行编码函数std::ranges::unique_copy(v, std::back_inserter(encoded)) 是范围版算法(第一个参数直接是 range,无需 begin/end)。最后的 encoded \| rv::filter(notZero) \| rv::take(4) \| std::ranges::to<std::string>() 展示了组合的魅力:take(4) 在取出第 4 个满足条件的字符后立即停止,天然实现”截断到 4 位”(配合物化)。课堂注释里还提到 C++26 的 rv::concat("0000") 用于补零。评价两极:ranges 让代码更声明式、更可读、报错更好;但它非常新(C++20/23/26 逐步完善)、编译器支持参差、某些场景比手写循环慢(可参考 The Terrible Problem of Incrementing a Smart Iterator)。A4 的 spellcheck 正是要求用 filter/transform 视图 + 物化完成。

与旧标准(如C++98)的对比

  • Lambda:C++11 引入。C++98 没有 lambda,只能手写具名 functor 类(定义 operator()、把状态存成员、手写构造函数)——std::sort 的自定义比较要专门写一个结构体。lambda 把这些样板全部语法糖化,让”就地定义行为”成为可能;捕获语法 [x][&] 也是 C++11 起才有(C++14 增加泛型 lambda 与 init-capture,C++20 增加模板参数 lambda)。
  • 函数指针:C++98 就有(继承自 C),如今仍可用,但”无状态”的硬伤使其在泛型编程中让位于 lambda/functor。
  • std::function:C++11 引入(Boost.Function 的标准化)。C++98 里想统一存放不同类型可调用对象几乎不可能。
  • Ranges 与 views:C++98 完全没有对应物——只能写 std::find(v.begin(), v.end(), x) 这样显式迭代器对 + 中间容器逐个物化的流水线。std::ranges::* 算法与 views 是 C++20 新增,std::ranges::to 是 C++23;它们把”组合算法”从命令式变成声明式。
  • Functors:C++98 就有(STL 最初的设计支柱),std::greater<T> 等至今未变——lambda 只是 functor 的语法糖,底层机制一直是它。

关键要点

  • 谓词 = 返回 bool 的函数:把”判断”当值传递,算法才能回答任意问题(find_if 而非 find)。
  • lambda 是携带状态的函数:捕获子句 [n]/[&] 决定状态按值快照还是按引用共享;lambda 本质是编译器生成的匿名 functor 类。
  • 能用标准算法就用标准算法:先手工演算 → 提炼逻辑 → 匹配 transform/copy_if/unique_copy 等,换取正确性与可读性(tokenizer 三步骤)。
  • 范围算法急切、views 惰性std::ranges::sort 立即执行;filter \| transform 只是配方,物化(to<> 或容器构造)时才逐元素计算。
  • auto + 模板优先,std::function 兜底:函数/lambda 的类型交给编译器推导;std::function 能做类型擦除但稍慢。

常见陷阱与注意事项

  • Lambda 引用捕获悬垂[&] 捕获的变量在 lambda 存活期内必须依然有效。若 lambda 逃逸出变量作用域(如存进容器返回出去),引用会悬垂。A4 中捕获 source 必须按引用Token 需要引用源字符串),但注意 source 的生命周期要覆盖 lambda 的使用。
  • 默认捕获 [=] 的语义:按值捕获的是当前值的快照,之后外层变量再变,lambda 内看不到;需要看到最新值就 [&] 或按引用捕获单个变量。
  • 函数指针无法携带状态:别试图用函数指针表达”小于运行时读到的 N”——这正是 lambda 存在的意义。
  • 视图悬垂:view 只是对底层 range 的”窗口”,底层 range 被销毁后遍历 view 是未定义行为(如对临时对象取 view 再长期保存)。std::ranges::to 物化要趁底层 range 还活着。
  • std::isspace 的歧义:直接写 isspace 可能匹配到 <locale> 里的模板重载导致类型推导失败;课堂/讲义建议写 std::isspace(必要时用全局 ::isspace 区分)。
  • ranges 是新特性std::ranges::to(C++23)在某些编译器/标准库下不可用——A4 明确要求”只用 C++20 特性”,物化请用容器迭代器构造函数,别用 std::ranges::to

关联作业提示

本讲与 A4: Ispell 直接相关(课堂上会带写前半部分)。A4 要求:tokenize 用传统 STL 算法、spellcheck 用 ranges 库,全程不允许 for/while 循环

  • tokenize(对应示例 4 的方法论):①用讲义提供的 find_all(内部就是 find_if 的循环)收集所有空白字符迭代器(含 begin/end 边界,谓词用 std::isspace);②用二元 std::transform 把”相邻两个迭代器”配对,lambda 形如 [&source](auto it1, auto it2) { return Token(source, it1, it2); }——必须按引用捕获 source(讲义特别警告,历史上很多同学栽在这里);③用 std::erase_if 删除空 Token(谓词 [](const auto& t) { return t.content.empty(); })。
  • spellcheck(对应示例 5/6):source \| rv::filter(拼写错误的 Token) \| rv::transform(Token → Misspelling) 三级流水线;生成建议时还要在 lambda 内嵌套 dictionary \| rv::filter(levenshtein(...) == 1) 视图,并用 std::set<std::string> suggestions(view.begin(), view.end()) 物化——不要用 std::ranges::to(C++23,超纲)。
  • 复习重点:捕获子句语法([&source] 为什么必须按引用)、filter/transform 两种调用形态(ranges::views::filter(r, pred)r \| views::filter(pred) 等价)、namespace rv = std::ranges::views; 别名技巧、以及”视图惰性、物化才计算”的心智模型。