第 16 章
函数重载与 Lambda:同名函数的挑选规则与匿名函数
重载只允许参数列表不同,返回类型不参与签名;lambda 是一个没有名字的函数对象,用捕获列表拿到外部变量——按值捕获是副本,按引用捕获是别名。
Function Overloading and Lambdas: Choosing Among Same-Named Functions, and Anonymous Functions
Overloads may differ only in their parameter lists; the return type is not part of the signature. A lambda is a nameless function object that reaches outer variables through a capture list — by value it holds a copy, by reference it holds an alias.
篇内目录
- What this chapter covers
- Timeline
- Function overloading
- The rule: only the parameter list distinguishes
- How the call picks one
- Lambdas: anonymous functions
- Syntax
- Naming and then calling
- Calling directly
- Parameters and return values
- Explicit return type
- Capture lists: how a lambda sees outer variables
- Capture by value
- Capture by reference
- Capturing everything
- The three styles compared
- Takeaways
- Comprehension check
- Relation to neighbouring chapters
这是 31 小时长视频的第 16 章,对应时间轴 16:34:13–17:41:39。前半段讲函数重载:同一个名字、不同的参数列表,编译器按实参挑选;后半段讲 lambda:把函数写成一个可以赋值、可以直接调用的表达式。
这一章在讲什么
两个机制解决同一个问题——让调用处的写法更自然:
- 重载让你写
max(a, b)时不必关心a、b是int、double还是std::string_view,编译器替你选。 - lambda 让你在需要函数的地方就地写一个,不必为了用一次而单独定义一个具名函数。
两者的共同前提都是第 14 章讲的签名:重载靠参数列表区分,lambda 的主体也是函数类型。
时间轴
| 时间 | 内容 |
|---|---|
| 16:34:13 | 重载规则:同名、参数列表不同 |
| 16:35:17 | 返回类型不参与区分 |
| 16:36:31 | 调用时按实参类型选重载 |
| 16:37:37 | VS Code:overloading with different parameters |
| 16:39:14 | 仅返回类型不同 → cannot overload function distinguished by the return type alone |
| 16:43:14 | 改变参数顺序、个数 |
| 16:47:06 | std::string_view 重载与字面量的隐式转换 |
| 16:48:52 | lambda 简介:匿名函数 |
| 16:49:51 | lambda 语法:捕获列表、参数、返回类型、函数体 |
| 16:52:15 | 用 auto 给 lambda 起名后调用 |
| 16:53:21 | 直接调用:在函数体后加 () |
| 16:54:19 | 带参数的 lambda |
| 16:55:07 | 返回值的 lambda 与嵌套在 std::cout 中 |
| 16:56:26 | 显式指定返回类型 -> int |
| 16:57:17 | VS Code:declaring and using lambda functions |
| 17:19:32 | lambda 小结 |
| 17:20:18 | 捕获列表:为什么需要 |
| 17:22:11 | 按值捕获:内部是副本 |
| 17:24:15 | 按引用捕获:内部是别名 |
| 17:25:16 | VS Code:capture lists |
| 17:34:01 | 捕获全部:= 与 & |
| 17:36:31 | VS Code:capture all |
| 17:40:21 | 下一章预告:函数模板 |
函数重载
规则:只有参数列表能区分
int max(int a, int b) { return a > b ? a : b; }
int max(int a, int b) { return a > b ? a : b; } // 错误:签名相同
编译器会直接拒绝:
cannot overload function distinguished by the return type alone
第 14 章已经说过:签名 = 名字 + 参数列表,不含返回类型。所以下面几种改法里,只有改参数列表是合法的:
| 改动 | 是否构成新重载 |
|---|---|
| 只改返回类型 | 否 |
| 只改参数名 | 否 |
| 改参数类型 | 是 |
| 改参数个数 | 是 |
| 改参数顺序(类型不同时) | 是 |
int max(int a, int b);
double max(double a, double b);
int max(int a, double b);
int max(double a, int b);
int max(int a, int b, int c);
调用时如何选择
int x{4}, y{9};
double a{5.4}, b{7.4};
std::cout << max(x, y) << "\n"; // int 重载
std::cout << max(a, b) << "\n"; // double 重载
编译器按实参的类型做匹配。注意两个细节:
auto result = max(x, y);的结果类型由被选中的重载决定——这里是int。- 隐式转换会参与匹配。
max("hello", "world")会尝试把字符串字面量转成std::string_view,因此调用string_view版本。
int max(std::string_view a, std::string_view b) {
return a > b ? 1 : -1; // 仅为演示重载选择
}
这也是重载容易出问题的地方:如果多组重载都能通过隐式转换匹配,编译器在「歧义」时会报错,而不是随便挑一个。
课件提醒:能写出
max(int,double)和max(double,int)不代表应该写。重载的价值是让调用处自然,不是展示语法。参数列表差异要有语义上的理由。
Lambda:匿名函数
语法
[capture](parameters) -> return_type { body }
四个部分按顺序:
| 部分 | 是否可省 | 说明 |
|---|---|---|
[...] 捕获列表 | 不可省(可空) | 让函数体访问外部变量 |
(...) 参数列表 | 不可省(可空) | 与普通函数相同 |
-> T 返回类型 | 可省 | 省略时由 return 语句推导 |
{...} 函数体 | 不可省 | 具体做什么 |
声明后命名调用
单独写一个 lambda 只是「定义了一个对象」,不会执行:
[]() {
std::cout << "Hello World\n";
}; // 声明了,但没调用
要反复调用,就把它赋给变量:
auto greet = []() {
std::cout << "Hello World\n";
};
greet();
greet(); // 可以调用多次
auto 推导出的类型是编译器为这个 lambda 生成的独一无二的闭包类型——你写不出它的名字,所以只能用 auto。
直接调用
只调用一次时,在函数体后直接加 ():
[]() {
std::cout << "Hello World\n";
}(); // 声明并立即调用
没有名字,也就没有第二次调用的机会。
带参数与返回值
[](double a, double b) {
std::cout << "a + b = " << a + b << "\n";
}(10.0, 5.0);
auto result = [](double a, double b) {
return a + b;
}(10.0, 20.0); // result == 30.0
最后一个写法值得注意:整个 lambda 表达式是一个值,可以直接放进 std::cout:
std::cout << "sum = " << [](double a, double b) { return a + b; }(3.0, 4.0) << "\n";
显式返回类型
auto f = [](double a, double b) -> int {
return a + b; // double 结果被转成 int
};
f(6.9, 3.5); // 返回 10,而不是 10.4
省略 -> T 时,返回类型由 return 表达式推导(这里是 double);写上 -> int 就强制转成 int。可以用 sizeof(result) 观察两者的差异:int 通常是 4 字节,double 是 8 字节。
捕获列表:lambda 如何看到外部变量
lambda 的函数体默认看不到外部作用域的局部变量:
double a{10}, b{20};
auto f = []() {
std::cout << a + b; // 编译错误:a、b 未被捕获
};
gcc 的报错是 'a' is not captured。原因是 lambda 本质上是一个独立的类类型,外部变量对它而言并不在作用域内。
按值捕获
double c{42};
auto f = [c]() {
std::cout << "inner: " << c << "\n"; // 捕获时的副本
};
for (int i{0}; i < 5; ++i) {
std::cout << "outer: " << c << "\n";
f();
++c;
}
输出里 outer 从 42 递增到 46,inner 始终是 42。打印地址会看到两个不同的位置:按值捕获在创建 lambda 的那一刻复制了一份。
按引用捕获
auto g = [&c]() {
std::cout << "inner: " << c << "\n";
};
加一个 & 就变成引用:内外地址相同,外部改 c,内部立刻看到;内部改 c,外部也改变。
捕获全部
[=]() { /* 所有用到的外部变量按值捕获 */ }
[&]() { /* 所有用到的外部变量按引用捕获 */ }
= 与 & 是简写,省去逐个列举。课件特意提醒:& 很危险——如果 lambda 活得比外部变量久,引用会悬空。捕获全部按引用只适合「lambda 的生命周期明显短于外部变量」的场合。
三种捕获对比
| 写法 | 内部拿到的是 | 外部修改是否影响内部 | 风险 |
|---|---|---|---|
[c] | 副本 | 否 | 无(但副本可能过期) |
[&c] | 引用 | 是 | 外部变量先销毁则悬空 |
[=] | 所有用到的变量副本 | 否 | 无 |
[&] | 所有用到的变量引用 | 是 | 同上,且不易察觉 |
本章结论
- 重载只允许参数列表不同:类型、个数、顺序;返回类型和参数名都不算。
- 调用时编译器按实参类型匹配,必要时插入隐式转换;多组同样匹配会报歧义。
- lambda 语法为
[捕获](参数) -> 返回类型 { 函数体 },返回类型可省略。 - lambda 可以赋给
auto变量反复调用,也可以在函数体后加()立即调用。 - 捕获列表让 lambda 访问外部变量:
[c]复制,[&c]引用,[=]/[&]全部捕获。 - 按引用捕获要保证 lambda 不会比被捕获的变量活得更久。
理解检验
1. 下面哪几行可以同时存在?
int max(int a, int b);
double max(int a, int b);
int max(int a, int b, int c);
int max(double a, int b);
参考答案:第 1 行与第 2 行签名相同(都是 max(int,int)),只有返回类型不同,不能共存;编译器会报 cannot overload function distinguished by the return type alone。第 3 行参数为三个 int,第 4 行参数为 double,int,签名都不同,可以与第 1 行共存。所以最终保留的是第 1、3、4 行。
2. 调用 max("hello", "world") 时,如果同时存在 max(std::string_view, std::string_view) 与 max(const char*, const char*) 两个重载,会发生什么?
参考答案:会选中 max(const char*, const char*)。字符串字面量的类型是 const char[N],退化为 const char* 属于标准转换;转成 std::string_view 则是用户定义转换。重载决议先比较转换序列的优劣,标准转换优于用户定义转换,所以 const char* 版本胜出,不会产生歧义。只有当两个候选的转换序列无法分出高下时,编译器才报歧义错误。
3. auto f = [](double a, double b) -> int { return a + b; }; 调用 f(6.9, 3.5) 返回什么?去掉 -> int 呢?
参考答案:带 -> int 时,a + b 先算得 10.4,再隐式转换成 int,结果是 10。去掉返回类型后由 return 表达式推导为 double,结果是 10.4。可以用 sizeof 观察:前者通常 4 字节,后者 8 字节。
4. 为什么下面这段代码里 inner 一直打印 42?
double c{42};
auto f = [c]() { std::cout << c << "\n"; };
for (int i{0}; i < 5; ++i) { f(); ++c; }
参考答案:[c] 是按值捕获,lambda 在创建时复制了一份 c,此后函数体读的是那份副本。循环里的 ++c 改的是外部变量,与副本无关,所以副本始终是 42。若改成 [&c],内部读到的就是外部变量,会随循环递增。
5. [&] 捕获全部按引用,什么情况下会出问题?
参考答案:当 lambda 的生命周期超过被捕获的局部变量时。例如把一个 [&] 的 lambda 存起来、在函数返回后再调用,此时它引用的局部变量已经销毁,访问就是悬空引用(未定义行为)。按值捕获 [=] 没有这个问题。这也说明 [&] 只适合「调用发生在被捕获变量仍然存活期间」的场合。
与前后章的关系
上一章讲参数传递,本章的 lambda 把「函数」本身变成可以传递的值;下一章的函数模板则走向另一个方向——用一份代码生成多个具体函数,解决「重载写了十几份、函数体却完全一样」的重复问题。捕获列表与模板参数,是后续把 lambda 传给算法的两块基础。
This is chapter 16 of a 31-hour single video, covering 16:34:13–17:41:39. The first half covers overloading: one name, several parameter lists, and the compiler picks by argument type. The second half covers lambdas: functions written as expressions that can be stored and called.
What this chapter covers
Both mechanisms solve the same problem — making call sites read naturally:
- Overloading lets you write
max(a, b)without caring whetheraandbareint,double, orstd::string_view; the compiler chooses. - Lambdas let you write a function right where it is needed instead of declaring a named one for a single use.
Both rest on the notion of a signature from chapter 14: overloads are distinguished by their parameter lists, and a lambda's body is a function type.
Timeline
| Time | Topic |
|---|---|
| 16:34:13 | Overloading rules: same name, different parameter lists |
| 16:35:17 | The return type does not distinguish overloads |
| 16:36:31 | The call selects an overload by argument type |
| 16:37:37 | In VS Code: overloading with different parameters |
| 16:39:14 | Return-type-only difference → cannot overload function distinguished by the return type alone |
| 16:43:14 | Changing parameter order and count |
| 16:47:06 | A std::string_view overload and implicit conversion from literals |
| 16:48:52 | Lambdas: anonymous functions |
| 16:49:51 | Lambda syntax: capture list, parameters, return type, body |
| 16:52:15 | Naming a lambda with auto and calling it |
| 16:53:21 | Calling directly by appending () |
| 16:54:19 | Lambdas with parameters |
| 16:55:07 | Lambdas that return values, and nesting one in std::cout |
| 16:56:26 | Explicit return type -> int |
| 16:57:17 | In VS Code: declaring and using lambda functions |
| 17:19:32 | Lambda summary |
| 17:20:18 | Capture lists: why they are needed |
| 17:22:11 | Capture by value: a copy inside |
| 17:24:15 | Capture by reference: an alias inside |
| 17:25:16 | In VS Code: capture lists |
| 17:34:01 | Capturing everything: = and & |
| 17:36:31 | In VS Code: capture all |
| 17:40:21 | Preview of the next chapter: function templates |
Function overloading
The rule: only the parameter list distinguishes
int max(int a, int b) { return a > b ? a : b; }
int max(int a, int b) { return a > b ? a : b; } // error: identical signature
The compiler refuses outright:
cannot overload function distinguished by the return type alone
Chapter 14 established this: a signature is the name plus the parameter list, excluding the return type. Of the possible changes, only parameter-list changes are legal:
| Change | New overload? |
|---|---|
| Return type only | no |
| Parameter names only | no |
| Parameter types | yes |
| Parameter count | yes |
| Parameter order (with different types) | yes |
int max(int a, int b);
double max(double a, double b);
int max(int a, double b);
int max(double a, int b);
int max(int a, int b, int c);
How the call picks one
int x{4}, y{9};
double a{5.4}, b{7.4};
std::cout << max(x, y) << "\n"; // int overload
std::cout << max(a, b) << "\n"; // double overload
The compiler matches on the types of the arguments. Two details matter:
auto result = max(x, y);takes the type of the selected overload — hereint.- Implicit conversions participate.
max("hello", "world")will convert the literals tostd::string_view, so that overload is called.
This is also where overloading goes wrong: if several candidates match equally well, the compiler reports ambiguity rather than picking arbitrarily.
The lecture's caution: being able to write
max(int,double)andmax(double,int)does not mean you should. Overloading exists to make call sites natural, not to display syntax. Differences in parameter lists should have a semantic reason.
Lambdas: anonymous functions
Syntax
[capture](parameters) -> return_type { body }
Four parts in order:
| Part | Optional? | Meaning |
|---|---|---|
[...] capture list | no (may be empty) | gives the body access to outer variables |
(...) parameters | no (may be empty) | same as an ordinary function |
-> T return type | yes | deduced from return when omitted |
{...} body | no | what it does |
Naming and then calling
A lambda written on its own merely defines an object; it does not run:
[]() {
std::cout << "Hello World\n";
}; // defined, never called
To call it repeatedly, assign it to a variable:
auto greet = []() {
std::cout << "Hello World\n";
};
greet();
greet(); // can be called many times
The type deduced by auto is a unique closure type that the compiler generates — its name cannot be written, so auto is the only option.
Calling directly
For a single use, append () after the body:
[]() {
std::cout << "Hello World\n";
}(); // define and call immediately
With no name there is no second call.
Parameters and return values
[](double a, double b) {
std::cout << "a + b = " << a + b << "\n";
}(10.0, 5.0);
auto result = [](double a, double b) {
return a + b;
}(10.0, 20.0); // result == 30.0
The last form is worth noting: the whole lambda expression is a value, so it can sit directly inside std::cout:
std::cout << "sum = " << [](double a, double b) { return a + b; }(3.0, 4.0) << "\n";
Explicit return type
auto f = [](double a, double b) -> int {
return a + b; // the double result is converted to int
};
f(6.9, 3.5); // returns 10, not 10.4
Without -> T, the return type is deduced from the return expression (here double); writing -> int forces a conversion. sizeof(result) shows the difference: int is usually 4 bytes, double 8.
Capture lists: how a lambda sees outer variables
A lambda body cannot see local variables from the enclosing scope by default:
double a{10}, b{20};
auto f = []() {
std::cout << a + b; // compile error: a and b are not captured
};
gcc reports 'a' is not captured. The reason is that a lambda is a self-contained class type; outer variables are not in its scope.
Capture by value
double c{42};
auto f = [c]() {
std::cout << "inner: " << c << "\n"; // the copy made at creation
};
for (int i{0}; i < 5; ++i) {
std::cout << "outer: " << c << "\n";
f();
++c;
}
outer climbs from 42 to 46 while inner stays at 42. Printing the addresses shows two distinct locations: capture by value copies the variable at the moment the lambda is created.
Capture by reference
auto g = [&c]() {
std::cout << "inner: " << c << "\n";
};
Adding & makes it a reference: the addresses match, external changes appear inside immediately, and writes inside are visible outside.
Capturing everything
[=]() { /* every used outer variable captured by value */ }
[&]() { /* every used outer variable captured by reference */ }
= and & are shorthands that avoid listing variables. The lecture warns that & is dangerous: if the lambda outlives the captured variable, the reference dangles. Capture-all-by-reference is safe only when the lambda's lifetime is clearly shorter than the variables'.
The three styles compared
| Form | Inside holds | Outer changes visible? | Risk |
|---|---|---|---|
[c] | a copy | no | none (the copy may go stale) |
[&c] | a reference | yes | dangles if the variable dies first |
[=] | copies of everything used | no | none |
[&] | references to everything used | yes | same as above, harder to spot |
Takeaways
- Overloads may differ only in parameter types, count, or order; return type and parameter names do not count.
- The call matches on argument types, inserting implicit conversions when needed; equally good candidates cause ambiguity.
- Lambda syntax is
[capture](parameters) -> return_type { body }, with the return type optional. - A lambda can be stored in an
autovariable and called repeatedly, or called immediately with trailing(). - Captures give access to outer variables:
[c]copies,[&c]refers,[=]and[&]capture all. - Capture by reference requires the captured variables to outlive the lambda.
Comprehension check
1. Which of these lines can coexist?
int max(int a, int b);
double max(int a, int b);
int max(int a, int b, int c);
int max(double a, int b);
Reference answer: lines 1 and 2 share the signature max(int,int) and differ only in return type, so they cannot coexist; the compiler reports cannot overload function distinguished by the return type alone. Line 3 takes three ints and line 4 takes double,int, so both have distinct signatures and may coexist with line 1. The surviving set is lines 1, 3, and 4.
2. If both max(std::string_view, std::string_view) and max(const char*, const char*) exist, which does max("hello", "world") call?
Reference answer: the const char* overload. A string literal has type const char[N], and decaying to const char* is a standard conversion, whereas converting to std::string_view is a user-defined conversion. Overload resolution compares conversion sequences, and a standard conversion beats a user-defined one, so the const char* version wins and no ambiguity arises. The compiler reports ambiguity only when neither candidate's sequence is better.
3. What does f(6.9, 3.5) return for auto f = [](double a, double b) -> int { return a + b; };, and what changes if -> int is removed?
Reference answer: with -> int, a + b evaluates to 10.4 and is then converted to int, giving 10. Without the explicit return type, the return expression deduces double, giving 10.4. sizeof shows the difference: 4 bytes versus 8 on typical platforms.
4. Why does inner keep printing 42 in the loop below?
double c{42};
auto f = [c]() { std::cout << c << "\n"; };
for (int i{0}; i < 5; ++i) { f(); ++c; }
Reference answer: [c] captures by value, so the lambda copies c when it is created and thereafter reads that copy. ++c modifies the outer variable, which the copy knows nothing about, so the copy remains 42. Change the capture to [&c] and the lambda reads the outer variable, which then climbs with the loop.
5. When does [&] capture-all-by-reference cause problems?
Reference answer: when the lambda outlives the captured local variables. Storing a [&] lambda and calling it after the enclosing function has returned leaves it referring to destroyed locals — a dangling reference and undefined behaviour. Capturing by value with [=] avoids this. [&] is appropriate only when the call happens while the captured variables are still alive.
Relation to neighbouring chapters
The previous chapter covered argument passing; this chapter turns a function itself into a value that can be passed around. The next chapter moves the other way: function templates write one blueprint and let the compiler generate concrete functions, solving the repetition of a dozen overloads with identical bodies. Capture lists and template parameters together are the groundwork for handing lambdas to algorithms.