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 lvalue 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.