Reputation: 9178
I am new to Rust and trying to understand how ownership is passed when objects are returned from functions. In the following reference based implementation , since reference doesn't have ownership , when "s" goes out of scope, it gets drops and deallocated.
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!
This is fixed by not returning the reference:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
Now I am trying to understand this with C++ implementation as follows:
std::string no_dangle() {
std::string s("hello world");
return s;
}
According to my understanding, in C++ when "s" is returned from the function, another copy is created using the copy-constructor and the "s" created inside the function get deallocated.This mean, two objects are created which not really optical in terms of the memory.
My questions:
In Rust, when "s" is returned from the function, no additional object is created.Only the ownership is returned.The original object allocated in the heap stays the same.Is this correct?
In C++, you can return "things" from functions by returning objects as well as pointer(smart pointer or raw pointer).But in Rust ,the only to return "things" is as above, which compared to C++ close to returning a smart-pointer?
Upvotes: 2
Views: 1214
Reputation: 5358
Both rust and C++ are value typed languages, so objects/structs are not allocated on the heap unless explicitly asked for. So in neither of the cases given is the string object/struct in question allocated on the heap. In both languages strings use a dynamically allocated backing buffer which is stored on the heap, but this is an important distinction.
So in rust, if you return by value, the object is moved, which is always equivalent to a straight memcpy, as rust structs are not allowed to have custom move logic, and cloning must be explicit. That memcopy copies the pointer to the backing storage, so that the string object may be in different memory, but the backing buffer remains the same.
In C++ objects can have non-trivial copy and (in C++11 and later) move constructors. So if this was anything other than returning a named value, then the copy or move constructor would have to be invoked. However, for the specific case of returning from a function, the copy elision rules come into play. This says that optionally (In C++17 and later it is required for some simple cases), if the object is initialized in the return statement, or comes from a location with automatic storage duration, then the compiler does not invoke the copy/move constructor, but instead the object is constructed directly into storage provided by the caller at the point the return object was originally created, meaning no copy or move is required at the point of return. This is known as Return Value Optimization.
If in C++11 or later you were to return a value that was not an object initialization or a named value with automatic storage duration (Or in those cases at compiler discretion except for object initialization in C++17 and later), such as the result of calling another function, then the move constructor would be invoked, in this case simply copying the pointer to the backing store and clearing the pointer in the old string. In which case the behavior would be just like rust. If the type had a more complex move constructor though, it could do anything as a result of the move.
Finally in C++98, if you were to return a value that was not an object initialization or a named value with automatic storage duration then the copy constructor would be invoked, the backing store copied to a new backing store, and that backing store returned. Resulting in a new string pointing to different memory. The old memory would then be freed by the destructor as the scope ended.
Additionally a C++ implementation may use the small string optimization where the small strings are stored directly in the string object. In which case there will be no backing store, and the string will have to be copied, even if the object is moved.
One final point to note is that prior to C++11 it was common for std::string
implementations to use a reference counted backing store. In which case the copy would increment the reference count on the backing store, and the destructor would decrement its increment, but not deallocate as there is still a reference to the store. In which case the resulting string would still point to the original backing store, but at the cost of a slightly more expensive process than moving. With the introduction of move constructors this has become less common.
For a quick answer to the second question, rust also allows returning smart pointers, pointers and references, however the rust borrow checker will prevent returning references to objects local objects as they will not have sufficient lifetime. This does not prevent returning references to parameters and globals (such as string literals or thread locals), as they have a longer lifetime than the function.
Upvotes: 6