Rust如何处理结构体作为函数参数和返回值?

21

我有一些C语言的经验,但是对于Rust来说还很新。当我将一个结构体传递到一个函数中并从函数中返回一个结构体时,在底层会发生什么?它似乎并没有“复制”结构体,但如果没有复制,那么结构体在哪里创建?它是在外部函数的堆栈中吗?

struct Point {
    x: i32,
    y: i32,
}

// I know it's better to pass in a reference here, 
// but I just want to clarify the point.
fn copy_struct(p: Point) { 
    // Is this return value created in the outer stack 
    // so it won't be cleaned up while exiting this function?  
    Point {.. p} 
}

fn test() {
    let p1 = Point { x: 1, y: 2 };
    // Will p1 be copied or does copy_struct 
    // just use a reference of the one created on the outer stack?
    let p2 = copy_struct(p1); 
}

只是想分享一篇有趣的关于类似主题的文章 http://blog.zgtm.de/1 - Jammy Lee
3个回答

32
作为长期从事C语言编程并最近尝试Rust语言的程序员,我理解您的出发点。对我来说,重要的是要理解在Rust中值和引用是关于所有权的,而且编译器可以调整调用约定以优化移动语义。
因此,你可以传递一个值而不需要在堆栈上复制,但是这将把所有权转移到被调用函数中。它仍然在调用函数的堆栈框架中,并且从C ABI的角度看,它正在传递一个指针,但是编译器强制执行返回时该值不再使用。
还有返回值优化,其中调用函数分配空间并将指针传递给调用者,后者可以直接填充返回值。这是一个C程序员通常手动处理的事情。
因此,所有权规则和借用检查器的安全性,结合缺乏固定的保证ABI / 调用约定,允许编译器生成高效的调用站点。通常您会更关注所有权和生命周期,而不需要试图聪明地处理函数调用堆栈行为。

0

我做了一些实验,发现当结构体的大小大于16时,即使该结构体具有Copy trait,rust也会进行返回值优化。

#[derive(Copy, Clone)]
struct NoRVO {
    _a: [u8; 16],
}

#[derive(Copy, Clone)]
struct HasRVO {
    _a: [u8; 17],
}

#[inline(never)]
fn new_no_rvo() -> NoRVO {
    let no_rvo = NoRVO { _a: [0; 16] };
    println!("callee no_rvo:  {:p}", &no_rvo);
    no_rvo
}

#[inline(never)]
fn new_has_rvo() -> HasRVO {
    let has_rvo = HasRVO { _a: [0; 17] };
    println!("callee has_rvo: {:p}", &has_rvo);
    has_rvo
}

fn main() {
    let has_rvo = new_has_rvo();
    println!("caller has_rvo: {:p}", &has_rvo);

    let no_rvo = new_no_rvo();
    println!("caller no_rvo:  {:p}", &no_rvo);
}

输出的一个例子:

callee has_rvo: 0x13791ff380
caller has_rvo: 0x13791ff380
callee no_rvo:  0x13791ff2b0
caller no_rvo:  0x13791ff3e8

0

我不确定你在问什么。

如果你的问题是关于从程序员角度看你所创建的值会发生什么,那么答案是它被移动了(除非它实现了Copy)。你可能需要通过一些基本的Rust教程来掌握这个概念。

如果你问的是底层发生了什么,那么恐怕没有一个单一的答案。我相信,在概念上,该值是使用类似于memcpy的东西进行复制的,但是优化器可能会介入并消除这个过程。我认为没有像规范这样的东西,最好将其视为实现细节。


“大型”返回值通过引用返回;我不确定参数,但如果“大型”按值传递的参数也是通过引用传递的话,我也不会感到惊讶。 - Francis Gagné
@FrancisGagné 你是什么意思?在这些情况下编译器是否聪明到足以为调用者堆栈分配返回值的内存? - kirelagin
1
是的,而且即使在调试构建中也会发生这种情况,以保持调用约定的一致性。更好的方法是,如果立即将结果放入一个盒子中,代码实际上会首先执行分配,然后将指向分配内存的指针传递给函数,并在那里写入返回值。不幸的是,目前,(只有不稳定的box关键字才是真实的);使用“Box :: new()”无法启用该优化。放置语法希望很快就能解决这个问题。 - Francis Gagné

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接