Lecture 11 (Week 6 - Tuesday): 函数与 Lambda (Functions & Lambdas)
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 是任何”有
begin和end“的东西(容器、自定义类型);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, ???) // 没有现成函数可用
}
代码做什么:把 isVowel、isGood 两个谓词直接传给 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_if 与 find 的差别: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 算法(发音相似的名字得到相同编码):过滤字母 → 编码 → 去连续重复 → 还原首字母 → 去零 → 补零截断。Robert 与 Rupert 都得到 R163。
特性机制解说:这是”识别标准算法”方法论的典范,每一行都是管道的一级:①copy_if + ::isalpha(谓词)把非字母滤掉,std::back_inserter(letters) 是输出迭代器,每写一个元素就 push_back 一个,免去手动扩容;②transform 把每个字母经 soundexEncode 变成数字字符,输入输出都是 letters(原地变换);③unique_copy 只消除连续重复(如 “LL”→”L”),这正是 Soundex 的”合并相邻同音”规则;④再一次 copy_if 用 notZero 谓词去掉 ‘0’(元音/H/W/Y 编码);⑤字符串拼接 "0000" 后 substr(0, 4) 保证定长 4。注意这套传统写法每一步都急切地物化一个完整容器(letters、unique、no_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::find、sort 等)是 <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 时,transform 向 filter 要元素,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;别名技巧、以及”视图惰性、物化才计算”的心智模型。
