Comments (4)
@flame4 感谢你的建议。
先回答一下你的疑问。
代码里的fn_ptr是一个函数指针类型(Function Pointer Type) 。这样创建实际上是一种强制转换。就是通过函数名hello和类型签名fn()
,强制将一个函数或者是没有捕获变量的闭包转换为函数指针类型。
函数指针,其实是来自于C语言的概念,它首先是一个指针,可以像一般函数一样,用于调用函数、传递参数。在Rust里,你直接用函数名字,就可以当函数指针使用。你结合示例理解,指针是可以通过{:p}
格式打印地址的,而非指针类型,则无法通过那个格式打印地址。
这里说「函数本身的类型」,是指函数项类型(Function Item Type)。你可以像下面这样修改代码清单6-14中那一行代码:
let other_fn: () = hello;
编译示例代码后,输出:
error[E0308]: mismatched types
--> src/main.rs:8:24
|
8 | let other_fn: () = hello;
| ^^^^^ expected (), found fn item
|
= note: expected type `()`
found type `fn() {hello}`
通过这个技巧,你可以看到,other_fn的类型是fn(){hello}
,这个类型是函数本身自有的类型,它不是指针。
如何挖掘知识
实际上,如果像这样深究细节的话,会有很多东西,一本书根本写不完的。书的目的,不是告诉你全部的细节,我更希望你通过学习本书的知识,自己挖掘出更多的细节。比如这个问题中,你既然已经看到了第六章,那是不是意味着你第五章已经看完了呢? 那说明你已经了解过MIR了。
所以,你为什么不能自己去精简一下代码,输出MIR自己研究下。像下面这样:
fn hello(){
1;
}
fn main(){
let fn_ptr: fn() = hello;
let other_fn = hello;
}
这样简化代码,是为了减少更多的认知障碍,比如println!语句会生成很多对你分析问题无用的MIR。
然后可以在playground里打印输出它的MIR:
fn hello() -> (){
let mut _0: (); // return place
let mut _1: i32;
bb0: {
_1 = const 1i32; // bb0[0]: scope 0 at src/main.rs:3:4: 3:5
// ty::Const
// + ty: i32
// + val: Scalar(Bits { size: 4, bits: 1 })
// mir::Constant
// + span: src/main.rs:3:4: 3:5
// + ty: i32
// + literal: Const { ty: i32, val: Scalar(Bits { size: 4, bits: 1 }) }
return; // bb0[1]: scope 0 at src/main.rs:4:2: 4:2
}
}
fn main() -> (){
let mut _0: (); // return place
scope 1 {
scope 3 {
}
scope 4 {
let _2: fn() {hello}; // "other_fn" in scope 4 at src/main.rs:7:9: 7:17
}
}
scope 2 {
let _1: fn() as UserTypeProjection { base: Ty(Canonical { variables: [], value: fn() }), projs: [] }; // "fn_ptr" in scope 2 at src/main.rs:6:9: 6:15
}
bb0: {
StorageLive(_1); // bb0[0]: scope 0 at src/main.rs:6:9: 6:15
_1 = const hello as fn() (ReifyFnPointer); // bb0[1]: scope 0 at src/main.rs:6:24: 6:29
// ty::Const
// + ty: fn() {hello}
// + val: Scalar(Bits { size: 0, bits: 0 })
// mir::Constant
// + span: src/main.rs:6:24: 6:29
// + ty: fn() {hello}
// + literal: Const { ty: fn() {hello}, val: Scalar(Bits { size: 0, bits: 0 }) }
StorageLive(_2); // bb0[2]: scope 1 at src/main.rs:7:9: 7:17
_2 = const hello; // bb0[3]: scope 1 at src/main.rs:7:20: 7:25
// ty::Const
// + ty: fn() {hello}
// + val: Scalar(Bits { size: 0, bits: 0 })
// mir::Constant
// + span: src/main.rs:7:20: 7:25
// + ty: fn() {hello}
// + literal: Const { ty: fn() {hello}, val: Scalar(Bits { size: 0, bits: 0 }) }
StorageDead(_2); // bb0[4]: scope 1 at src/main.rs:9:1: 9:2
StorageDead(_1); // bb0[5]: scope 0 at src/main.rs:9:1: 9:2
return; // bb0[6]: scope 0 at src/main.rs:9:2: 9:2
}
}
可以通过这个MIR,就看得出来
- hello,是一个函数指针类型 (ReifyFnPointer),因为 _1 =const hello as fn()(ReifyFnPointer); ,通过as,将hello转换为
fn()
类型的函数指针。 - 而other_fn 是函数类型(fn(){hello }), _2 =const hello; ,它并没有被转换为函数指针类型。
但是,你如果这么写:
let other_fn: fn() = hello;
other_fn就会被转换为一个函数指针类型。
另外,值得注意的是:
// + ty: fn() {hello}
// + val: Scalar(Bits { size: 0, bits: 0 })
从生成的MIR中,可以看得出来,函数指针类型和函数类型,类型签名都是fn(){hello} 。并且它们的值,都是零大小的(Scalar代表具体存储的值)。只不过,函数指针类型,是被强制转换为了指针。而函数类型,并没有被转换为指针。
有的人有疑问,函数指针类型怎么是零大小的?继续深度挖掘一下。
// src/librustc/mir/mod.rs
pub enum CastKind {
Misc,
/// Convert unique, zero-sized type for a fn to fn()
ReifyFnPointer,
/// Convert non capturing closure to fn()
ClosureFnPointer,
/// Convert safe fn() to unsafe fn()
UnsafeFnPointer,
/// "Unsize" -- convert a thin-or-fat pointer to a fat pointer.
/// codegen must figure out the details once full monomorphization
/// is known. For example, this could be used to cast from a
/// `&[i32;N]` to a `&[i32]`, or a `Box<T>` to a `Box<dyn Trait>`
/// (presuming `T: Trait`).
Unsize,
}
实际上,普通函数会经过一个ReifyFnPointer方式的转换。这种方式会将零大小类型的普通函数转换为函数指针类型。MIR代码中赋值语句可以这么理解:
_1 = (const hello as fn()) (ReifyFnPointer);
//等价于
_1 = cast(hello, fn(), ReifyFnPointer);
将hello转换为fn()类型,转换方式是ReifyFnPointer。
同样,可以看到,用于将未捕获闭包转换为函数指针类型的转换方式是ClosureFnPointer。UnsafeFnPointer方式用于将safe的普通函数指针转成unsafe函数指针类型。而这里的Unsize是将指针转为胖指针的方式。
再继续将上面的代码转成LLVM IR。
start:
%other_fn = alloca {}, align 1
%fn_ptr = alloca void ()*, align 8
可以看到,函数项类型(fn-item type)other_fn是零大小的。而fn_ptr已经被转换成了指针类型,是要占用空间的。而otherfn只是函数名hello,而fn_ptr是一个ReifyFnPointer的强转。
那么此时这个问题「Rust中函数名是什么」的答案,已经冒出:是函数项类型(Fn-Item Type)。
当普通函数作为函数参数传递的时候,是会显式标记签名类型,就会被转换为函数指针类型。
fn hello(){
1;
}
fn world(f: fn()){
f();
}
fn main(){
let fn_ptr: fn() = hello;
let other_fn = hello;
world(hello);
}
零成本抽象
Rust里有很多零大小类型,包括单元值、单元结构体等。这里函数类型和函数指针类型同样都是零大小类型。
Rust这个函数指针类型和C/CPP中的函数名表达式是一致的,都是函数指针。但是在C/CPP中使用函数指针,想做到零开销还是有困难,因为函数指针在运行时占用空间,如果想降低开销只能依赖于代码优化。
Rust中的函数都实现了 FnOnce/FnMut/Fn 这三个 Trait ,所以对于下面的函数:
fn call_fn<F: FnOnce()>(f: F) { f() }
参数f也可以传入一个普通函数,此时,f的行为可以在编译期完全确定。 所以,为了最大化地利用编译期已知信息,必须可以通过类型F携带函数f调用所需的必要信息。而不是通过函数指针类型来调用。后者不符合Rust零成本抽象的原则,并且还需要进行额外的一个指针大小的参数传递。
所以 Rust 的做法是,函数和类型构造器(枚举值和元组结构体)的名字表达式,都有一个零大小的,只在类型里记录函数信息的值。这个值就叫做 函数项(Function item),它的类型就叫做 函数项类型(Function item type)。
并且,向上面的示例那样,该值可以通过显式地标记函数类型签名来强制转换到同函数签名的函数指针类型。但没有特别的必要,不要进行这种转换。因为函数项才是最高效的。一旦使用了函数项,剩下的优化就依赖于对零大小类型的优化了。
从上面示例中也看得出来,Rust的优化是分两个阶段的:MIR阶段和LLVM阶段。
小结:
任何一本书,都不可能囊括其主题内容的全部细节。看书学习的过程,也是一个再创造的过程,给自己一个机会去挖掘去创造更多知识。
以上。如果有错误,欢迎反馈。最后,感谢 知乎上林吟风 和读者群朋友 KevinWang的深度反馈,很棒!
@flame4 建议很好,我考虑在第二版中再看看如何增加解释比较好。但毕竟,细节太多了,书现在已经很多内容了。。。
from tao-of-rust-codes.
@flame4 重新修改了上面的答案。
from tao-of-rust-codes.
@ZhangHanDong 感谢作者的回答, 清晰深入! 我倒是的确没有看到第五章的MIR内容, 我是看完官方文档了, 所以跳着看来加深理解的, 我后面看一下那部分内容. 这个知识点如果很深入的话, 我的建议是这个位置简单说一下深层的结论和如何去探索的index就足够了, 毕竟对于读者来说, 肯定没有您这么深厚的知识体系, 看到这里就能联想到全书内容, 很有可能想深入了解一下却不知道该怎么办, 增加一点指引的内容, 告诉读者去看哪一节可以更深入理解, 对一个热爱思考和探索的读者, 我觉得就足够了.
再次感谢作者大大细致耐心的回答
from tao-of-rust-codes.
@flame4 上面的回答又更新了,之前有些错误,可以再看一下。你的建议我会考虑在第三次印刷中完善。再次感谢你的反馈。
from tao-of-rust-codes.
Related Issues (20)
- [代码错误] 6.3 迭代器 代码清单6-80
- 第四章内存管理 结构体A的内存对齐前后的布局对比 图4-8绘制似乎是错误的,请确认 HOT 2
- 第44页的代码清单2-5注释错误 HOT 1
- 2.5.1 节,if判断不建议使用小括号 HOT 1
- 4.2.3 代码清单4-18链表初始化语句有重复操作,报错信息有更新 HOT 2
- 2.10.2 节 动态分发写法在新版本中已经废弃 HOT 1
- 6.2.1 闭包的基本用法 相同写法的空闭包可以放在vec中 HOT 1
- 6.3.5 消费其 any 不再是一次遍历到底 利用try_fold 支持break HOT 3
- 第二章2.3.3 关于所有权转移代码示例可能有误 HOT 3
- 问题:如何正确地hook系统函数? HOT 1
- 第五章 多个生命周期参数 HOT 1
- cannot specialize default item `swim` HOT 1
- 13.2.2 子类型和型变问题 HOT 1
- 第三章 函数重载应改为函数重写 HOT 1
- [第三章]代码清单3-36的trait对象在最新stable1.66报错,trait对象应改为&dyn Bar HOT 1
- 13.2.2 491页 “step1函数第一个参数&val生命周期本来是'a,因为协变而变成'static,所以借用检查就正常通过了” HOT 1
- [第5章] 5.5.4 代码清单5-42中trait对象在2021版本中需要在trait名称前加上dyn HOT 1
- [第5章] 5.6 代码清单5-47注释中foo不存在
- [第5章] 5.8代码清单5-62中生成的MIR代码与1.67 不一致,建议更新或采用不同分支提供相应代码 HOT 1
- [第7章]7.1.1结构体的代码清单7-9中input没有必要是mut,而且try应该用问号替代 HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from tao-of-rust-codes.