【问题标题】:Rust vs C++ : Returning objects from functionsRust vs C++:从函数返回对象
【发布时间】:2020-07-19 12:17:16
【问题描述】:

我是 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 有 type state,用于跟踪所有权。并且 Rust 已经在编译时检查了强大的所有权语言要求。 C++ 两者都没有。所以比较 Rust 和 C++ 有点像苹果和橘子。
  • 将第二个问题作为一个单独的问题提出可能是个好主意,仅回答第一个问题需要一个完整的长答案来解释在 C++ 中返回字符串的详细信息。
  • 关于第 2 点:Rust 也有智能指针,Box 与 C++ unique_ptr 大致相同,Rc 与 C++ shared_ptr 大致相同。在 Rust 中返回内容的唯一方法就是您在此处显示的内容,这不是真的 - 您还可以返回指向堆上对象的 BoxRc

标签: c++ rust


【解决方案1】:

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 借用检查器将阻止返回对对象本地对象的引用,因为它们没有足够的生命周期。这不会阻止返回对参数和全局变量(例如字符串文字或线程局部变量)的引用,因为它们的生命周期比函数长。

【讨论】:

  • 我认为在这里提一些术语很重要:我们正在谈论 RVO(返回值优化)和 NRVO(命名返回值优化)。清除这一点后,我的理解是,从一开始,C++ 标准就在编译器的帮助下允许 RVO 和 NRVO(即复制省略),而 C++17 只在一些容易检测到的情况下强制执行。此外,复制省略意味着什么都没有发生,既不复制也不移动。
  • @MatthieuM。这就是我在第二段中试图解释的内容,我只是想不出一种方法来将这些术语与影响的解释结合起来。至于复制省略,我已经澄清我指的是原始结构,而不是返回点。
猜你喜欢
  • 2021-08-09
  • 2014-05-15
  • 1970-01-01
  • 2012-02-29
  • 2021-01-30
  • 2021-09-28
  • 2014-07-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多