我是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”时,将使用copy-constructor创建另一个副本,并在函数内部创建的“ s”被释放。这意味着,创建了两个对象,它们内存方面真的很光学。
我的问题:
在Rust中,当从函数返回“ s”时,不会再创建任何对象。仅返回所有权。分配在堆中的原始对象保持不变。这是正确的吗?
在C ++中,您可以通过返回对象和指针(智能指针或原始指针)从函数中返回“事物”。但是在Rust中,唯一返回“事物”的方法如上所述,与C ++即将返回智能指针了吗?
答案 0 :(得分:3)
rust和C ++都是值类型的语言,因此除非明确要求,否则对象/结构不会在堆上分配。因此,在两种情况下都不会在堆上分配有问题的字符串对象/结构。在这两种语言中,字符串都使用动态分配的后备缓冲区,该缓冲区存储在堆中,但这是一个重要的区别。
因此在rust中,如果按值返回,则对象将被移动,该对象始终等效于平直的memcpy,因为rust结构不允许具有自定义移动逻辑,并且克隆必须是明确的。该内存复制将指针复制到后备存储器,以便字符串对象可能位于不同的内存中,但后备缓冲区保持不变。
在C ++中,对象可以具有非平凡的副本和(在C ++ 11及更高版本中)移动构造函数。因此,如果除了返回命名值以外,还必须调用copy或move构造函数。但是,对于从函数返回的特定情况,复制省略规则起作用。这表示可以选择(在C ++ 17及更高版本中,对于某些简单情况是必需的),如果对象是在return语句中初始化的,或者来自具有自动存储持续时间的位置,则编译器不会调用copy / move构造函数,但是在最初创建返回对象时,该对象直接构造到调用者提供的存储中,这意味着在返回点不需要复制或移动。这就是返回值优化。
如果在C ++ 11或更高版本中,您将返回不是对象初始化的值或具有自动存储持续时间的命名值(或者在这种情况下,由编译器自行决定,但C ++ 17和更高版本中的对象初始化除外) )(例如调用另一个函数的结果),然后将调用move构造函数,在这种情况下,只需将指针复制到后备存储并清除旧字符串中的指针即可。在这种情况下,行为就像生锈。如果该类型具有更复杂的move构造函数,则由于该移动它可以做任何事情。
最后,在C ++ 98中,如果要返回的值不是对象初始化值或具有自动存储持续时间的命名值,则将调用复制构造函数,将后备存储复制到新的后备存储,并且那家后备店回来了。导致指向不同内存的新字符串。然后,作用域结束时,析构函数将释放旧的内存。
另外,C ++实现可以使用小字符串优化,其中小字符串直接存储在字符串对象中。在这种情况下,将没有后备存储,即使移动了对象,也必须复制字符串。
最后要注意的一点是,在C ++ 11之前,std::string
实现通常使用引用计数的后备存储。在这种情况下,副本将增加后备存储上的引用计数,而析构函数将减少其增量,但不会取消分配,因为仍然存在对该存储的引用。在这种情况下,生成的字符串仍将指向原始的后备存储,但是所花费的过程要比迁移的过程稍微昂贵。随着move构造函数的引入,这种情况已不再普遍。
为快速回答第二个问题,rust还允许返回智能指针,指针和引用,但是rust借用检查器将防止返回对对象本地对象的引用,因为它们的寿命不足。这不会阻止返回对参数和全局变量(例如字符串文字或线程本地变量)的引用,因为它们的寿命比函数长。