《Rust编程之道》读者答疑精选:函数项类型和函数指针类型

《Rust编程之道》读者答疑精选:函数项类型和函数指针类型

这是来自于《Rust编程之道》读者问题之一,觉得有必要分享一下。因为从这个问题中,挖掘出了新的知识点,可以作为本书的补充。

具体链接: 函数指针类型的一点建议 · Issue #42 · ZhangHanDong/tao-of-rust-codes

这篇文章可以算是我和读者朋友的共同创作。


问题:

相关代码:

// 代码清单6-14 函数指针
fn hello(){
    println!("hello function pointer");
}
fn main(){
    let fn_ptr: fn() = hello;
    println!("{:p}", fn_ptr); // 0x562bacfb9f80
    let other_fn = hello;
    // println!("{:p}", other_fn);  // not function pointer
    hello();
   other_fn();
   fn_ptr();
   (fn_ptr)();
}

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

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

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


回答:


代码里的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。用于将safe的普通函数指针转成unsafe函数指针类型的UnsafeFnPointer。而这里的Unsize是将指针转为胖指针。

再继续将上面的代码转成LLVM IR。

start:
  %other_fn = alloca {}, align 1
  %fn_ptr = alloca void ()*, align 8

可以看到,函数项类型(fn-item type)other_fn是零大小的。而fn_ptr已经被转换成了指针类型,是要占用空间的。而other_fn只是函数名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的深度反馈,很棒!

编辑于 2019-01-10

文章被以下专栏收录