Type promotions and conversions
lvalue to rvalue
In C++ lvalues are automatically converted to rvalues when needed.
In Rust the equivalent of lvalues are "place expressions" (expressions that represent memory locations) and the equivalent of rvalues are "value expressions". Place expressions are automatically converted to value expressions when needed.
int main() {
// Local variables are lvalues,
int x(0);
// and therefore may be assigned to.
x = 42;
// x is converted to an rvalue when needed.
int y = x + 1;
}
fn main() { // Local variables are place expressions, let mut x = 0; // and therefore may be assigned to. x = 42; // x is converted to a value expression when // needed. let y = x + 1; }
Array to pointer
In C++, arrays are automatically converted to pointers as required.
The equivalent to this in Rust is the automatic conversion of vector and array references to slice references.
#include <cstring>
int main() {
char example[6] = "hello";
char other[6];
// strncpy takes arguments of type char*
strncpy(other, example, 6);
}
fn third(ts: &[char]) -> Option<&char> { ts.get(2) } fn main() { let vec: Vec<char> = vec!['a', 'b', 'c']; let arr: [char; 3] = ['a', 'b', 'c']; third(&vec); third(&arr); }
Because slice references can be easily used in a memory-safe way, it is generally recommended in Rust to define functions in terms of slice references instead of in terms of references to vectors or arrays, unless vector-specific or array-specific functionality is needed.
Unlike in C++ where the conversion from arrays to pointers is built into the
language, this is actually a general mechanism provided by the Deref
trait, which provides one
kind of user-defined conversion.
Function to pointer
In C++ functions and static member functions are automatically converted to function pointers.
Rust performs the same conversion. In addition to functions and members that do
not take self as an argument, constructors (proper constructors) also have
function type and can be converted to function pointers. Non-capturing closures
do not have function type, but can also be converted to function pointers.
int twice(int n) {
return n * n;
}
struct MyPair {
int x;
int y;
MyPair(int x, int y) : x(x), y(y) {}
static MyPair make() {
return MyPair{0, 0};
}
};
int main() {
// convert a function to a function pointer
int (*twicePtr)(int) = twice;
int result = twicePtr(5);
// Per C++23 11.4.5.1.6, can't take the address
// of a constructor.
// MyPair (*ctor)(int, int) = MyPair::MyPair;
// MyPair pair = ctor(10, 20);
// convert a static method to a function
// pointer
MyPair (*methodPtr)() = MyPair::make;
MyPair pair2 = methodPtr();
// convert a non-capturing closure to a
// function pointer
int (*closure)(int) = [](int x) -> int {
return x * 5;
};
int closureRes = closure(2);
}
fn twice(x: i32) -> i32 { x * x } struct MyPair(i32, i32); impl MyPair { fn new() -> MyPair { MyPair(0, 0) } } fn main() { // convert a function to a function pointer let twicePtr: fn(i32) -> i32 = twice; let res = twicePtr(5); // convert a constructor to a function pointer let ctorPtr: fn(i32, i32) -> MyPair = MyPair; let pair = ctorPtr(10, 20); // convert a static method to a function // pointer let methodPtr: fn() -> MyPair = MyPair::new; let pair2 = methodPtr(); // convert a non-capturing closure to a // function pointer let closure: fn(i32) -> i32 = |x: i32| x * 5; let closureRes = closure(2); }
Numeric promotion and numeric conversion
In C++ there are several kinds of implicit conversions that occur between numeric types. The most commonly encountered are numeric promotions, which convert numeric types to larger types.
These lossless conversions are not implicit in Rust. Instead, they must be
performed explicitly using the Into::into() method. These conversions are
provided by implementations of the
From and
Into traits. The list
of conversions provided by the Rust standard library is listed on the
documentation
page for
the trait.
int main() {
int x(42);
long y = x;
float a(1.0);
double b = a;
}
fn main() { let x: i32 = 42; let y: i64 = x.into(); let a: f32 = 1.0; let b: f64 = a.into(); }
There are several implicit conversions that occur in C++ that are not lossless. For example, integers can be implicitly converted to unsigned integers in C++.
In Rust, these conversions are also required to be explicit and are provided by
the TryFrom and
TryInto traits
which require handling the cases where the value does not map to the other type.
int main() {
int x(42);
unsigned int y(x);
float a(1.0);
double b(a);
}
use std::convert::TryInto; fn main() { let x: i32 = 42; let y: u32 = match x.try_into() { Ok(x) => x, Err(err) => { panic!("Can't convert! {:?}", err); } }; }
Some conversions that occur in C++ are supported by neither From nor TryFrom
because there is not a clear choice of conversion or because they are not
value-preserving. For example, in C++ int32_t can implicitly be converted to
float despite float not being able to represent all 32 bit integers
precisely, but in Rust there is no TryFrom<i32> implementation for f32.
In Rust the only way to convert from an i32 to an f32 is with the as
operator.
The operator can actually be used to convert between other primitive types as
well and does not panic or produce undefined behavior, but may not convert in
the desired way (e.g., it may use a different rounding mode than desired or it
may truncate rather than saturate as desired).
#include <cstdint>
int main() {
int32_t x(42);
float a = x;
}
fn main() { let x: i32 = 42; let a: f32 = x as f32; }
isize and usize
In the Rust standard library the isize and usize types are used for values
intended to used be indices (much like size_t in C++). However, their use for
other purposes is usually discouraged in favor of using explicitly sized types
such as u32. This results a situation where values of type u32 have to be
converted to usize for use in indexing, but Into<usize> is not implemented
for u32.
In these cases, best practice is to use TryInto, and if further error handling
of the failure cause is not desired, to call unwrap, creating a panic at the
point of conversion.
This is preferred because it prevents the possibility of moving forward with an
incorrect value. E.g., consider converting a u64 to a usize that has a
32-bit representation with as, which truncates the result. A value that is one
greater than the u32::MAX will truncate to 0, which would probably result in
successfully retrieving the wrong value from a data structure, thus masking a
bug and producing unexpected behavior.
Enums
In C++ enums can be implicitly converted to integer types.
In Rust the conversion requires the use of the as operator, and providing
From and TryFrom implementations to move back and forth between the enum and
its representation type is recommended. Examples and additional details are
given in the chapter on enums.
Qualification conversion
In C++ qualification conversions enable the use of const (or volatile) values where the const (or volatile) qualifier is not expected.
In Rust the equivalent enables the use of mut variables and mut references
to be used where non-mut variables or references are expected.
#include <iostream>
#include <string>
void display(const std::string &msg) {
std::cout << "Displaying: " << msg << std::endl;
}
int main() {
// no const qualifier
std::string message("hello world");
// used where const expected
display(message);
}
fn display(msg: &str) { println!("{}", msg); } fn main() { let mut s: String = "hello world".to_string(); let message: &mut str = s.as_mut(); display(message); }
Integer literals
In C++ integer literals with no suffix indicating type have the smallest type in
which they can fit from int, long int, or long long int. When the literal
is then assigned to a variable of a different type, an implicit conversion is
performed.
In Rust, integer literals have their type inferred depending on context. When
there is insufficient information to infer a type either i32 is assumed or may
require some type annotation to be given.
#include <cstdint>
#include <iostream>
int main() {
// Compiles without error (but with a warning).
uint32_t x = 4294967296;
// assumes int
auto y = 1;
// literal is given a larger type, so it prints
// correctly
std::cout << 4294967296 << std::endl;
// these work as expected
std::cout << INT64_C(4294967296) << std::endl;
uint64_t z = INT64_C(4294967296);
std::cout << z << std::endl;
}
fn main() { // error: literal out of range for `u32` // let x: u32 = 4294967296; // assumes i32 let y = 1; // fails to compile because it is inferred as i32 // print!("{}", 4294967296); // These work, though. println!("{}", 4294967296u64); let z: u64 = 4294967296; println!("{}", z); }
Safe bools
The safe bool idiom exists to make it possible to use types as conditions. Since C++11 this idiom is straightforward to implement.
In Rust instead of converting the value to a boolean, the normal idiom matches
on the value instead. Depending on the situation, the mechanism used for
matching might be match, if let, or let else.
struct Wire {
bool ready;
unsigned int value;
explicit operator bool() const { return ready; }
};
int main() {
Wire w{false, 0};
// ...
if (w) {
// use w.value
} else {
// do something else
}
}
enum Wire { Ready(u32), NotReady, } fn main() { let wire = Wire::NotReady; // ... // match match wire { Wire::Ready(v) => { // use value v } Wire::NotReady => { // do something else } } // if let if let Wire::Ready(v) = wire { // use value v } // let else let Wire::Ready(v) = wire else { // do something that doesn't continue, // like early return return; }; }
User-defined conversions
User-defined conversions are covered in a separate chapter.
Click here to leave us feedback about this page.