第 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.

首发于 2022.02.17中 / EN
系统课程cppoverloadinglambdacapture-listclosures
语言
字号
目录
篇内目录

这是 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:37VS Code:overloading with different parameters
16:39:14仅返回类型不同 → cannot overload function distinguished by the return type alone
16:43:14改变参数顺序、个数
16:47:06std::string_view 重载与字面量的隐式转换
16:48:52lambda 简介:匿名函数
16:49:51lambda 语法:捕获列表、参数、返回类型、函数体
16:52:15用 auto 给 lambda 起名后调用
16:53:21直接调用:在函数体后加 ()
16:54:19带参数的 lambda
16:55:07返回值的 lambda 与嵌套在 std::cout 中
16:56:26显式指定返回类型 -> int
16:57:17VS Code:declaring and using lambda functions
17:19:32lambda 小结
17:20:18捕获列表:为什么需要
17:22:11按值捕获:内部是副本
17:24:15按引用捕获:内部是别名
17:25:16VS Code:capture lists
17:34:01捕获全部:= 与 &
17:36:31VS 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 重载

编译器按实参的类型做匹配。注意两个细节:

  1. auto result = max(x, y); 的结果类型由被选中的重载决定——这里是 int。
  2. 隐式转换会参与匹配。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 whether a and b are int, double, or std::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

TimeTopic
16:34:13Overloading rules: same name, different parameter lists
16:35:17The return type does not distinguish overloads
16:36:31The call selects an overload by argument type
16:37:37In VS Code: overloading with different parameters
16:39:14Return-type-only difference → cannot overload function distinguished by the return type alone
16:43:14Changing parameter order and count
16:47:06A std::string_view overload and implicit conversion from literals
16:48:52Lambdas: anonymous functions
16:49:51Lambda syntax: capture list, parameters, return type, body
16:52:15Naming a lambda with auto and calling it
16:53:21Calling directly by appending ()
16:54:19Lambdas with parameters
16:55:07Lambdas that return values, and nesting one in std::cout
16:56:26Explicit return type -> int
16:57:17In VS Code: declaring and using lambda functions
17:19:32Lambda summary
17:20:18Capture lists: why they are needed
17:22:11Capture by value: a copy inside
17:24:15Capture by reference: an alias inside
17:25:16In VS Code: capture lists
17:34:01Capturing everything: = and &
17:36:31In VS Code: capture all
17:40:21Preview 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:

ChangeNew overload?
Return type onlyno
Parameter names onlyno
Parameter typesyes
Parameter countyes
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:

  1. auto result = max(x, y); takes the type of the selected overload — here int.
  2. Implicit conversions participate. max("hello", "world") will convert the literals to std::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) and max(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:

PartOptional?Meaning
[...] capture listno (may be empty)gives the body access to outer variables
(...) parametersno (may be empty)same as an ordinary function
-> T return typeyesdeduced from return when omitted
{...} bodynowhat 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

FormInside holdsOuter changes visible?Risk
[c]a copynonone (the copy may go stale)
[&c]a referenceyesdangles if the variable dies first
[=]copies of everything usednonone
[&]references to everything usedyessame 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 auto variable 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.

来源