In C++, lvalues are copied, while rvalues can be moved, if the type
actually implements move operations (and I am glossing over a lot of details
here).

There is a function in C++ std library that allows us to transform any
lvalue to rvalue, called std::move.

So, we can modify our previous C++ program to behave similarly to Rust program
and avoid unnecessary copy by wrapping val with std::move:

#include <string>
usingstd::string;voidfoo(stringval){// val is destroyed here
}intmain(){stringval("Hello");foo(std::move(val));// warning: accessing val here is NOT fine!
// original val is also destroyed here, but contains no value so it's fine
}

Note that std::move does not actually move anything, it just changes how
the compiler treats the value at this particular place. In this case, move works
because std::string implements move operations.

In C++, it is possible to accidentally use moved value. Therefore, the move
operations usually set the original container size to zero.

Therefore, a good practice in C++ is to avoid using move in the case like
this, even if this means unnecessary deep copy of the value, to avoid the
accidental usage of the moved value.

If the copy of value is actually costly and should not be copied, it is worth
wrapping it into unique_ptr (like Box) or shared_ptr (like Arc),
which will keep a single instance of the value on the heap. Relying on move
in such case is very fragile and incurs a maintenance cost to keep the program
correct.

Functions and Methods

Const references

In Rust, you can create a function that immutably borrows a value:

fnfoo(value:&String){println!("value: {}",value);}

The Rust compiler will not allow calling methods or operations on String that
modify contents of that String. In Rust-talk it would not allow to call methods
that mutably borrow a string or need to take ownership of a string.

The const T& idiom is similar to &T in Rust. C++ compiler will
not allow modifying the contents of const T& object. In C++-talk, the C++
would not allow to call methods on the string that are non-const.

Const methods

Let’s say we have structure Person in Rust, and use it as parameter for function
print_full_name:

However, the caller of foo must do the same to ensure the move, and this cycle
continues.

One thing to look for when using std::move is mutable
references! Let’s say we had a mutable reference in function foo, and moved
the value:

voidfoo(Person&person,std::string&name){person.set_name(std::move(name));// move clears the original name
}

Now the caller of foo will suddenly find the name gone.

In this particular case, the better practice is to use const T& reference
all the way down to the setter. This will create a copy of name inside
the setter, with a minimal overhead.

However, if the name was a very big string, i.e. something like file contents,
and it would be necessary to ensure no copies for performance reasons, the
unique_ptr or shared_ptr would come to the rescue:

References are basically pointers. The lifetime syntax &'a mut communicates to the
compiler that the returned value must point to the same or narrower memory location 'a as the function
argument.

If we tried to return a reference to the value which is outside of 'a, the compiler would complain:

implPerson{pubfnget_first_name_mut(&'amutself)->&'amutString{&mutString::from("Other")// error: borrowed value does not live long enough// ^^^^^^^^^^^^^^^^^^^^^ temporary value created here}}

Therefore, at the call site, the compiler knows that the Person is borrowed
for every call to append_foo and would not allow us to do anything funky:

fnmain(){letmutp=Person{first_name:String::from("John"),last_name:String::from("Smith"),};{letname:&mutString=p.get_first_name_mut();p.first_name=String::from("Crash");// error: cannot assign to `p.first_name` because it is borrowedappend_foo(name);}}

The C++, however, has no machinery to understand where the pointers or references
point to, and does not help. However, we can still implement the same in C++.

However, the C++ compiler is not able to track lifetimes and ensure memory
safety.

This is a problem when you get used to these things being verified by the compiler.
The objects we have just written might become more complex, and it would
become much harder to track runaway modifications
to Person:

It worked, even when we have overwritten the memory location of
Person. This actually may continue working. Or it may fail in release build.
Or it may fail when other developer wraps Person in shared_ptr:

Conclusion

The big hurdle when moving back to C++ from Rust was the missing move-by-default
feature. This required learning other idiomatic patterns in C++ land, and in some
cases admitting that not all the code needs to be both efficient and easy to maintain.

In most cases maintainability wins, and avoiding “premature optimization” is
very much a necessity in C++.