首页 > 解决方案 > Rust vs C++:从函数返回对象

问题描述

我是 Rust 的新手,并试图了解从函数返回对象时如何传递所有权。在以下基于引用的实现中,由于引用没有所有权,因此当“s”超出范围时,它会被丢弃并释放。

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

这是通过不返回引用来解决的:

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

现在我试图用 C++ 实现来理解这一点,如下所示:

std::string no_dangle() {
    std::string s("hello world");
    return s;
}

根据我的理解,在 C++ 中,当从函数返回“s”时,使用复制构造函数创建另一个副本,并且在函数内部创建的“s”被释放。这意味着,创建了两个对象,这不是真正的光学记忆的条款。

我的问题:

  1. 在 Rust 中,当函数返回“s”时,不会创建额外的对象。只返回所有权。堆中分配的原始对象保持不变。这样正确吗?

  2. 在 C++ 中,您可以通过返回对象以及指针(智能指针或原始指针)从函数中返回“事物”。但在 Rust 中,唯一返回“事物”的方式如上,与 C++ 相比接近返回智能-指针?

标签: c++rust

解决方案


rust 和 C++ 都是值类型语言,因此除非明确要求,否则不会在堆上分配对象/结构。因此,在这两种情况下,都没有在堆上分配有问题的字符串对象/结构。在这两种语言中,字符串都使用存储在堆上的动态分配的后备缓冲区,但这是一个重要的区别。

所以在 rust 中,如果按值返回,对象会被移动,这始终等同于直接的 memcpy,因为 rust 结构不允许有自定义移动逻辑,并且克隆必须是显式的。该 memcopy 将指针复制到后备存储,因此字符串对象可能位于不同的内存中,但后备缓冲区保持不变。

在 C++ 中,对象可以有非平凡的复制和(在 C++11 和更高版本中)移动构造函数。因此,如果这不是返回命名值,则必须调用复制或移动构造函数。但是,对于从函数返回的特定情况,复制省略规则会发挥作用。这表示可选(在 C++17 及更高版本中,某些简单情况需要),如果对象在 return 语句中初始化,或者来自具有自动存储持续时间的位置,则编译器不会调用复制/ move 构造函数,而是将对象直接构造到调用者在最初创建返回对象时提供的存储中,这意味着在返回时不需要复制或移动。这称为返回值优化。

如果在 C++11 或更高版本中您要返回一个不是对象初始化的值或具有自动存储持续时间的命名值(或者在那些情况下由编译器自行决定,除了 C++17 和更高版本中的对象初始化),例如作为调用另一个函数的结果,将调用移动构造函数,在这种情况下,只需将指针复制到后备存储并清除旧字符串中的指针。在这种情况下,行为就像生锈一样。如果该类型有一个更复杂的移动构造函数,它可以做任何移动的结果。

最后在 C++98 中,如果您要返回一个不是对象初始化的值或具有自动存储持续时间的命名值,则将调用复制构造函数,将后备存储复制到新的后备存储,然后该后备存储回来。导致新字符串指向不同的内存。当作用域结束时,析构函数将释放旧内存。

此外,C++ 实现可以使用小字符串优化,其中小字符串直接存储在字符串对象中。在这种情况下,将没有后备存储,并且必须复制字符串,即使对象被移动。

最后要注意的一点是,在 C++11 之前,实现通常std::string使用引用计数的后备存储。在这种情况下,副本将增加后备存储上的引用计数,而析构函数将减少其增量,但不会释放,因为仍然存在对存储的引用。在这种情况下,生成的字符串仍将指向原始的后备存储,但代价是比移动稍微昂贵的过程。随着移动构造函数的引入,这变得不那么常见了。

为了快速回答第二个问题,rust 还允许返回智能指针、指针和引用,但是 rust 借用检查器将阻止返回对对象本地对象的引用,因为它们没有足够的生命周期。这不会阻止返回对参数和全局变量(例如字符串文字或线程局部变量)的引用,因为它们的生命周期比函数长。


推荐阅读