Skip to content

Commit

Permalink
Update reference-type.md: restructure chapter
Browse files Browse the repository at this point in the history
  • Loading branch information
dynilath committed Jul 28, 2024
1 parent 6fccfa2 commit 7481fed
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 14 deletions.
4 changes: 4 additions & 0 deletions src/zh/03-types/reference-type/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: 3.5 引用类型
index: false
---
149 changes: 149 additions & 0 deletions src/zh/03-types/reference-type/function-ref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
title: 3.5.2 函数引用
---

可以使用引用绑定到函数:

```cpp
int add(int a, int b) {
return a + b;
}

auto& add_ref = add; // add_ref 是 add 函数的引用

int result = add_ref(1, 2); // 调用 add 函数
```
这里的 `add_ref` 是 `add` 函数的引用,可以像函数一样调用。这种引用称为**函数的引用**。
函数引用也可以作为函数的参数,例如:
```cpp
int apply(auto& func, int a, int b) {
return func(a, b);
}
int result = apply(add, 1, 2);
```

在这里,`apply` 函数接受一个函数引用作为参数,然后调用这个函数。

## 函数类型与函数引用类型

读者可以发现,在上面的例子中,`apply` 函数中的参数 `auto& func` 也能绑定到对象。此时:
```cpp
int a = 1;
int result = apply(a, 2, 3); // ![!code error] // 错误,a 不是函数
```
这样就会产生错误,因为 `a` 不是函数。但是这个错误会出现在 `apply` 函数里面,在调用方并不会产生提示。
此时,我们就会希望通过类型来限制 `func` 只能绑定到函数。这就需要使用函数类型,以及函数引用类型。

函数类型的形式是
```cpp
return_type (parameter_type1, parameter_type2, ...)
```
这里的 `return_type` 是函数的返回值类型,`parameter_type1, parameter_type2, ...` 是函数的参数类型。例如:
```cpp
using binary_int_func = int(int, int);
```
这样,`binary_int_func` 就是一个函数类型,它接受两个 `int` 类型的参数,返回一个 `int` 类型的值。

在我们声明了 `binary_int_func` 之后,就可以使用这个类型来限制 `func` 的类型:
```cpp
// 通过在 binary_int_func 后面加上 & 来组成一个函数引用类型
int apply(binary_int_func& func, int a, int b) {
return func(a, b);
}

int a = 1;
apply(a, 2, 3); // [!code error] // 错误,a 不是函数

float add(float a, float b) {
return a + b;
}
apply(add, 2, 3); // [!code error] // 错误,add 类型不匹配
```
这样,在调用 `apply` 函数时,就会提示无法用 `a` 初始化 `apply` 的第一个参数。这在 `apply` 函数的实现比较复杂的时候能够有效的提升分析错误的效率。
在声明函数类型时,并不能推导出函数调用类型为何,因此在声明函数类型时,必须显式指定参数类型,而不能使用 `auto`。例如:
```cpp
using binary_int_func = int(auto, auto); // [!code error] // 错误,auto 不能用在这里
```

但是,我们可以通过函数类型来确定一个声明中有 `auto` 的函数的形式:
```cpp
auto add(auto a, auto b) { // 这个函数的参数和返回值类型尚不确定
return a + b;
}

using binary_int_func = int(int, int);
int apply(binary_int_func& func, int a, int b) {
return func(a, b);
}

int result = apply(add, 2, 3); // 正确,用 add 初始化 func
```
这里,使用 `add` 初始化 `func` 时,会被推导为 `int(int, int)`,从而确定了 `add` 的参数和返回值类型。
::: info 你的第一个模板函数
考虑如下的代码:
```cpp
auto add(auto a, auto b) {
return a + b;
}
```
技术性地说,这里的 `add` 函数是一个函数模板,而非函数。当函数存在参数的类型(返回值没有影响)声明为 `auto` 时,这个函数就是一个模板。
这样设计使得这个 `add` 可以适应于各种各样的参数,并且以和计算结果相同的方式返回出来。

`add` 函数的两个参数类型被确定时,这个函数模板就会被自动推导成为一个对应的函数,例如:
```cpp
auto result = add(1, 2); // result 是 int 类型
auto result2 = add(1.0, 2.0); // result2 是 double 类型
auto result3 = add(1, 2.0); // result3 是 double 类型
```
你可能会觉得,这不就是把运算符 `+` 变成了个函数吗,但我们可以考虑一些更复杂的计算,例如[简单的控制台程序](../02-program-structure/cli-program.md)中的 `sqrt` 函数:
```cpp
auto sqrt(auto x) {
auto a = x;
while (a * a > x) {
a = (a + x / a) / 2;
}
return a;
}

auto result = sqrt(314159ull); // result 是 unsigned long long 类型
```
这时候,无论 `x` 是什么整数类型,`sqrt` 函数都能选择对应类型的局部对象 `a` 的类型,并返回对应类型的结果。而不需要费劲写下 `sqrt` `sqrtus` `sqrtul` `sqrtull` 等一系列函数,不用在调用的时候推敲要用哪个函数,不用再担惊受怕会不会因为类型不匹配发生数据溢出错误。
:::
::: info 直接声明函数引用类型与旧式类型别名
在前面,我们为了限制函数引用的类型,使用了一个类型别名来表达函数类型:
```cpp
using binary_int_func = int(int, int);
```

函数引用类型当然也可以使用类型别名:
```cpp
using binary_int_func_ref = binary_int_func&;
```

如果要直接声明一个函数引用类型,其形式就会变得复杂:
```cpp
using binary_int_func_ref = int(&)(int, int);
```

为什么在这里?因为 `&` 作为一个修饰符应当出现在那个标识符的前面,而函数的参数应当出现在标识符的后面,这种期待声明、类型、调用的形式统一的设计是从 C 语言中继承下来的。

使用[旧式类型别名](./type-intro.md#旧式类型别名) `typedef` 时,这种体现会更加明显:
```cpp
typedef int binary_int_func(int, int); // 声明了一个函数类型
typedef int (&binary_int_func_ref)(int, int); // 声明了一个函数引用类型

int add(int,int); // 函数声明,为了简便起见,省略了函数体
```
:::
## 在返回值中使用函数引用
::: important TODO: 补充内容
:::
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: 3.5 引用类型
title: 3.5.1 对象引用
---

**引用**是 C++ 中的一种特殊类型,它用来指代一个对象,或者一个函数。例如:
Expand Down Expand Up @@ -176,7 +176,7 @@ ref = 2; // 为什么?这不是字面量吗?
```
你可能会奇怪,为什么右值引用绑定到字面量之后,这个右值引用还能修改?右值引用不是右值吗?

首先,右值引用的名字本身,是一个标识符表达式,当然是左值
首先,右值引用的名字本身,是一个标识符组成的基础表达式,当然是左值表达式

此外,`ref` 所绑定到的对象,实际上是一个从 `1` 构造而来的临时对象。为了将一个右值表达式绑定给右值引用,会将右值表达式转换为一个与引用类型匹配的[将亡值表达式](./value-category.md)
将亡值表达式代指这个临时的对象,这个临时的对象的生命周期会延长到绑定到它的引用的生命期结束。举例而言:
Expand All @@ -194,7 +194,7 @@ auto&& ref = a; // ref 是 int& 类型
auto&& ref2 = 1; // ref2 是 int&& 类型
```

这一原理及其应用我们会在后面的章节中详细介绍
这一原理及其应用会在后面的章节中详细介绍

### 函数参数中的右值引用

Expand All @@ -209,7 +209,7 @@ void set_a_to(int&& value) {
这个 `set_a_to` 函数接受一个右值引用,可以接受右值表达式作为参数,例如:
```cpp
set_a_to(1);
set_a_to(a + 1); // a + 1 是右值表达式
set_a_to(a + 1); // a + 1 是纯右值表达式
```

当然,这个函数不接受左值表达式作为参数,例如:
Expand Down Expand Up @@ -239,6 +239,11 @@ const auto& ref = 1; // ref 是 const int& 类型
const auto& ref2 = a; // ref2 也是 const int& 类型
```

::: info 为什么允许常量左值引用绑定到右值?
当我们使用常量左值引用时,我们无法通过这个引用修改绑定的对象。因此,通过常量左值引用访问一个对象的过程,可以理解为我们只需要这个对象的值,而不需要知道这个对象在哪里占用什么内存。这就和我们使用算术表达式时一样,在计算 `1 + 2 + 3` 的时候,我们不需要知道 `1 + 2` 的结果存储在哪里,只需要知道这个结果是 `3` 就行,足够我们继续计算。
因此,就像在其他地方使用右值一样,我们也应当允许常量左值引用绑定到右值,使我们能够更方便地使用这个值。
:::

藉由常量左值引用,我们可以设计一个接受左值表达式也接受右值表达式的函数:
```cpp
int a;
Expand All @@ -254,10 +259,6 @@ set_a_to(a); // a 是左值,也可以传入
这个 `set_a_to` 函数当然也可以使用 `int` 类型作为参数,但当类型更为复杂,构造参数的成本也不得不被考虑时,常量左值引用的作用就会凸显出来。
::: info 为什么允许常量左值引用绑定到右值?
当我们使用常量左值引用时,我们无法通过这个引用修改绑定的对象。因此,通过常量左值引用访问一个对象的过程,可以理解为我们只需要这个对象的值,而不需要知道这个对象在哪里占用什么内存。这就和我们使用算术表达式时一样,在计算 `1 + 2 + 3` 的时候,我们不需要知道 `1 + 2` 的结果存储在哪里,只需要知道这个结果是 `3` 就行,足够我们继续计算。
因此,就像在其他地方使用右值一样,我们也应当允许常量左值引用绑定到右值,使我们能够更方便地使用这个值。
:::
## 悬垂引用
Expand Down Expand Up @@ -298,9 +299,3 @@ const int& ref2 = max(ref, 3); // ![!code error] // 错误,试图使用悬垂
因此,代码规范往往要求不要返回引用,或者返回引用时要确保引用绑定的对象的生命期足够长。
:::
## 函数的引用
::: important TODO: 补充内容
:::

0 comments on commit 7481fed

Please sign in to comment.