I’m following Rust by Example to finally get into Rust properly, after several years spent as a primarily C++ developer. So far, most things have made sense to me, and I have been blown away by the compiler diagnostics: they are accurate and useful, which anybody with some C++ experience will appreciate: C++ compilers, due to the sheer complexity of the language, have little choice but to write you a novel even for straight-forward errors such as missing a semicolon in a header file.
This is the first time though when Rust got me scratching my head for a second: when learning about Closure captures this gave me a pause:
let haystack = vec![1, 2, 3];
let contains = move |needle| haystack.contains(needle);
println!("{}", contains(&1));
I am, of course, talking about &1
. Just putting 1
does not work, the compiler will tell you expected '&{integer}', found integer
and suggests borrowing, i.e. writing &1
instead. I was not the only one to find this odd: References to Literals in Rust?! .
To be fair, this does work:
let haystack = vec![1, 2, 3];
let contains = move |needle| haystack.contains(&needle); // borrow here
println!("{}", contains(1)); // ... instead of here
Still, it struck me as odd needing to take a reference to an integer literal… until I realized that C++ is actually the same, except the syntax is more implicit, in that you can just write haystack.contains(1)
even if Vec::contains()
expects an immutable reference or constant reference in C++ terminology: this is because C++ rvalues (temporaries) bind to constant lvalue references, and indeed, lifetime extension occurs: the lifetime of the temporary is extended as if it was a regular local variable.
In Rust, the situation is exactly the same, with the difference being that Rust wants to make things like this explicit. In C++, when you see haystack.contains(foo)
, you have no idea if foo
is:
- passed by value, i.e. copied
- passed by lvalue reference, i.e. mutable reference
- passed by const lvalue reference, i.e. immutable reference
The syntax of C++ simply does not distinguish between these cases on the call site! In Rust, you have to make this explicit, similarly to defining Vec::contains()
to take a pointer: then even in C++ you’d have to write haystack.contains(&foo)
(unless foo
was already a pointer, of course). Rust is just making sure that it is obvious on the call site how you intend to pass the value.
Of course, in C++, were Vec::contains()
take a pointer instead of a const reference, then you’d not be able to call it with an integer literal. You’d first have to create a local variable:
int temp = 1;
haystack.contains(&temp);
This is what actually your code compiles to as well when Vec::contains()
takes a reference (const or not): on the hardware level, there is no such thing as a reference, only memory addresses or pointers, except untyped: so basically void*
or intptr_t
. Unless the compiler is able to inline the function call, for instance via link-time optimizations (LTO).
So C++ and Rust are the same, except that Rust requires you to explicitly write on the call site whether you are passing by reference or not, and if you want to pass a temporary (rvalue or literal) as a reference then it automatically defines the hidden local variable for you, as a convenience / quality-of-life feature. So it all makes sense, in the end!