New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

「第六章」函数指针类型的一点建议 #42

Open
flame4 opened this Issue Jan 9, 2019 · 4 comments

Comments

2 participants
@flame4
Copy link

flame4 commented Jan 9, 2019

页码与行数

  • 第169页
  • 第10行

书中在这里讲到 let other_fn = hello; 这里的类型是fn() {hello} 是这个函数本身的类型而不是函数指针类型, 后面还说到, “传入sum和product函数名之后, 会自动通过模式匹配转换为函数指针类型”

读到这里的时候, 有点不太理解 “函数本身的类型” 和 函数指针类型在rust内的具体区别, 以及, 是哪个trait/语言特性导致了 函数类型和函数指针的相互转换?

函数调用的时候如果传入一个函数名, 到底传入的是什么? 这里的细节还希望作者可以能多深入写两句. 因为写了函数本身的类型后, 我读后面的内容老是会纠结这两个概念在每个地方实际上是什么样子的.

仅仅是个人建议~

@ZhangHanDong

This comment has been minimized.

Copy link
Owner

ZhangHanDong commented Jan 9, 2019

@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,就看得出来

  1. hello,是一个函数指针类型 (ReifyFnPointer),因为 _1 =const hello as fn()(ReifyFnPointer); ,通过as,将hello转换为fn()类型的函数指针。
  2. 而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 建议很好,我考虑在第二版中再看看如何增加解释比较好。但毕竟,细节太多了,书现在已经很多内容了。。。

@ZhangHanDong

This comment has been minimized.

Copy link
Owner

ZhangHanDong commented Jan 9, 2019

@flame4 重新修改了上面的答案。

@flame4

This comment has been minimized.

Copy link

flame4 commented Jan 10, 2019

@ZhangHanDong 感谢作者的回答, 清晰深入! 我倒是的确没有看到第五章的MIR内容, 我是看完官方文档了, 所以跳着看来加深理解的, 我后面看一下那部分内容. 这个知识点如果很深入的话, 我的建议是这个位置简单说一下深层的结论和如何去探索的index就足够了, 毕竟对于读者来说, 肯定没有您这么深厚的知识体系, 看到这里就能联想到全书内容, 很有可能想深入了解一下却不知道该怎么办, 增加一点指引的内容, 告诉读者去看哪一节可以更深入理解, 对一个热爱思考和探索的读者, 我觉得就足够了.

再次感谢作者大大细致耐心的回答

@ZhangHanDong

This comment has been minimized.

Copy link
Owner

ZhangHanDong commented Jan 10, 2019

@flame4 上面的回答又更新了,之前有些错误,可以再看一下。你的建议我会考虑在第三次印刷中完善。再次感谢你的反馈。

@ZhangHanDong ZhangHanDong changed the title 函数指针类型的一点建议 「第六章」函数指针类型的一点建议 Jan 10, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment