Casting#
- Source:
Rust provides several mechanisms for type conversion, each with different safety
guarantees. Unlike C++, Rust has no implicit numeric conversions - every conversion
must be explicit. This prevents subtle bugs from unintended narrowing or widening
conversions. Rust’s type conversion system is built around traits (From, Into,
TryFrom, TryInto) that provide a consistent interface for both infallible
and fallible conversions. The as keyword is reserved for primitive type casts
where the conversion rules are well-defined by the language.
Casting Comparison#
The following table maps C++ casting mechanisms to their Rust equivalents. Note that
Rust deliberately omits some C++ casts - dynamic_cast isn’t needed because Rust
uses trait objects with explicit dyn syntax, and reinterpret_cast requires
unsafe code in Rust:
C++ |
Rust |
|---|---|
|
|
Conversion constructor |
|
|
|
|
No direct equivalent |
|
|
Primitive Casting with as#
The as keyword performs primitive type conversions similar to C++’s
static_cast. However, unlike C++, Rust never performs implicit numeric
conversions - you must always use as explicitly. This example shows basic
numeric conversions:
C++:
#include <iostream>
int main() {
int i = 42;
// Explicit casts
double d = static_cast<double>(i);
char c = static_cast<char>(i);
// Implicit conversions (allowed in C++, can be surprising)
double d2 = i; // implicit widening
short s = i; // implicit narrowing (may warn)
unsigned int u = -1; // implicit sign conversion (dangerous!)
std::cout << "d: " << d << ", c: " << static_cast<int>(c) << "\n";
std::cout << "d2: " << d2 << ", s: " << s << ", u: " << u << "\n";
return 0;
}
Rust:
fn main() {
let i: i32 = 42;
// Explicit casts required
let d: f64 = i as f64;
let c: u8 = i as u8;
// No implicit conversions - these won't compile:
// let d2: f64 = i; // error: expected f64, found i32
// let s: i16 = i; // error: expected i16, found i32
// let u: u32 = -1_i32; // error: expected u32, found i32
// Must be explicit about everything
let d2: f64 = i as f64;
let s: i16 = i as i16;
let u: u32 = (-1_i32) as u32; // wraps to u32::MAX
println!("d: {}, c: {}", d, c);
println!("d2: {}, s: {}, u: {}", d2, s, u);
}
as Casting Rules#
The as keyword follows specific rules for different conversion types. Numeric
conversions may truncate or wrap, float-to-int truncates toward zero, and pointer
casts are allowed between compatible types. This example demonstrates the various
as casting behaviors:
C++:
#include <cstdint>
#include <iostream>
int main() {
// Narrowing: truncates high bits
int32_t x = 1000;
uint8_t y = static_cast<uint8_t>(x); // 232 (1000 % 256)
// Float to int: truncates toward zero
double f = 3.9;
int i = static_cast<int>(f); // 3
double neg = -3.9;
int neg_i = static_cast<int>(neg); // -3
// Pointer to integer
int value = 42;
int* ptr = &value;
uintptr_t addr = reinterpret_cast<uintptr_t>(ptr);
std::cout << "y: " << static_cast<int>(y) << "\n";
std::cout << "i: " << i << ", neg_i: " << neg_i << "\n";
std::cout << "addr: " << addr << "\n";
return 0;
}
Rust:
fn main() {
// Narrowing: truncates high bits (wrapping behavior)
let x: i32 = 1000;
let y: u8 = x as u8; // 232 (1000 % 256)
println!("1000 as u8 = {}", y);
// Float to int: truncates toward zero
let f: f64 = 3.9;
let i: i32 = f as i32; // 3
println!("3.9 as i32 = {}", i);
let neg: f64 = -3.9;
let neg_i: i32 = neg as i32; // -3
println!("-3.9 as i32 = {}", neg_i);
// Pointer casts
let value: i32 = 42;
let ptr: *const i32 = &value;
let addr: usize = ptr as usize;
println!("Address: {:#x}", addr);
// Reference to raw pointer
let r: &i32 = &value;
let raw: *const i32 = r as *const i32;
println!("Raw pointer: {:?}", raw);
}
From and Into Traits#
From and Into are traits for infallible type conversions. They’re the
idiomatic way to convert between types in Rust, similar to C++ conversion
constructors but more explicit and composable. Implementing From<T> for a type
automatically provides the Into<T> implementation for free. These traits are
used extensively in the standard library and are the preferred way to handle
type conversions:
C++ (conversion constructor):
#include <iostream>
class Fahrenheit; // forward declaration
class Celsius {
public:
double value;
explicit Celsius(double v) : value(v) {}
// Conversion constructor from Fahrenheit
explicit Celsius(const Fahrenheit& f);
void print() const {
std::cout << value << "°C";
}
};
class Fahrenheit {
public:
double value;
explicit Fahrenheit(double v) : value(v) {}
// Conversion constructor from Celsius
explicit Fahrenheit(const Celsius& c)
: value(c.value * 9.0 / 5.0 + 32.0) {}
void print() const {
std::cout << value << "°F";
}
};
// Define after Fahrenheit is complete
Celsius::Celsius(const Fahrenheit& f)
: value((f.value - 32.0) * 5.0 / 9.0) {}
int main() {
Fahrenheit body_temp(98.6);
Celsius c(body_temp); // explicit conversion
std::cout << "Body temperature: ";
body_temp.print();
std::cout << " = ";
c.print();
std::cout << "\n";
return 0;
}
Rust:
#[derive(Debug)]
struct Celsius(f64);
#[derive(Debug)]
struct Fahrenheit(f64);
// Implement From<Fahrenheit> for Celsius
impl From<Fahrenheit> for Celsius {
fn from(f: Fahrenheit) -> Self {
Celsius((f.0 - 32.0) * 5.0 / 9.0)
}
}
// Implement From<Celsius> for Fahrenheit
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let body_temp = Fahrenheit(98.6);
// Using From::from explicitly
let c1 = Celsius::from(Fahrenheit(98.6));
println!("Body temperature: {:?} = {:?}", body_temp, c1);
// Using Into (automatically available when From is implemented)
let f2 = Fahrenheit(212.0);
let c2: Celsius = f2.into();
println!("Boiling point: 212°F = {:?}", c2);
// Into is useful in generic contexts
fn print_celsius(temp: impl Into<Celsius>) {
let c = temp.into();
println!("Temperature: {:.1}°C", c.0);
}
print_celsius(Fahrenheit(32.0)); // accepts Fahrenheit
print_celsius(Celsius(0.0)); // also accepts Celsius (From<T> for T)
}
Common From Implementations#
The standard library provides many From implementations for common conversions.
These make it easy to convert between related types without explicit casting:
C++:
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
int main() {
// String from literal
std::string s1 = "hello"; // implicit conversion
std::string s2("hello"); // explicit construction
// Vector from initializer list
std::vector<int> v = {1, 2, 3};
// Path from string
std::filesystem::path p = "/tmp/file.txt";
std::cout << "String: " << s1 << "\n";
std::cout << "Vector size: " << v.size() << "\n";
std::cout << "Path: " << p << "\n";
return 0;
}
Rust:
use std::path::PathBuf;
fn main() {
// String from &str
let s1: String = String::from("hello");
let s2: String = "hello".into(); // equivalent
let s3: String = "hello".to_string(); // also common
println!("Strings: {}, {}, {}", s1, s2, s3);
// Vec from array
let v1: Vec<i32> = Vec::from([1, 2, 3]);
let v2: Vec<i32> = [1, 2, 3].into();
println!("Vectors: {:?}, {:?}", v1, v2);
// PathBuf from string
let p1: PathBuf = PathBuf::from("/tmp/file.txt");
let p2: PathBuf = "/tmp/file.txt".into();
println!("Paths: {:?}, {:?}", p1, p2);
// Box from value
let boxed: Box<i32> = Box::from(42);
let boxed2: Box<i32> = 42.into();
println!("Boxed: {}, {}", boxed, boxed2);
// String from number (via ToString trait, not From)
let num_str = 42.to_string();
println!("Number as string: {}", num_str);
}
TryFrom and TryInto#
For conversions that might fail, Rust provides TryFrom and TryInto traits.
These return Result<T, E> instead of the converted value directly, forcing
callers to handle potential errors. This is similar to C++ functions that return
std::optional or throw exceptions:
C++:
#include <cstdint>
#include <iostream>
#include <optional>
#include <stdexcept>
// Return optional for fallible conversion
std::optional<uint8_t> try_from_int(int value) {
if (value < 0 || value > 255) {
return std::nullopt;
}
return static_cast<uint8_t>(value);
}
// Or throw exception
uint8_t from_int_throwing(int value) {
if (value < 0 || value > 255) {
throw std::out_of_range("Value out of u8 range");
}
return static_cast<uint8_t>(value);
}
int main() {
// Using optional
if (auto result = try_from_int(100)) {
std::cout << "Converted: " << static_cast<int>(*result) << "\n";
}
if (auto result = try_from_int(1000)) {
std::cout << "This won't print\n";
} else {
std::cout << "Conversion failed for 1000\n";
}
// Using exception
try {
auto val = from_int_throwing(1000);
} catch (const std::out_of_range& e) {
std::cout << "Exception: " << e.what() << "\n";
}
return 0;
}
Rust:
use std::convert::TryFrom;
fn main() {
let big: i32 = 1000;
// TryFrom returns Result - must handle error
let result: Result<u8, _> = u8::try_from(big);
match result {
Ok(val) => println!("Converted: {}", val),
Err(e) => println!("Conversion failed: {}", e),
}
// Using ? operator in functions that return Result
let fits: i32 = 100;
let small: u8 = u8::try_from(fits).expect("Value should fit");
println!("Converted 100 to u8: {}", small);
// TryInto is also available
let big2: i32 = 1000;
let result2: Result<u8, _> = big2.try_into();
println!("TryInto result: {:?}", result2);
}
Custom TryFrom#
You can implement TryFrom for your own types to provide fallible conversions
with custom error types. This is useful for validation during construction:
C++:
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
class PositiveInt {
int value_;
public:
// Factory function returning optional
static std::optional<PositiveInt> try_from(int value) {
if (value > 0) {
return PositiveInt(value);
}
return std::nullopt;
}
// Or throwing constructor
explicit PositiveInt(int value) : value_(value) {
if (value <= 0) {
throw std::invalid_argument("value must be positive");
}
}
int get() const { return value_; }
};
int main() {
// Using optional factory
if (auto pos = PositiveInt::try_from(42)) {
std::cout << "Created: " << pos->get() << "\n";
}
if (auto neg = PositiveInt::try_from(-1)) {
std::cout << "This won't print\n";
} else {
std::cout << "Failed to create from -1\n";
}
return 0;
}
Rust:
use std::convert::TryFrom;
use std::fmt;
#[derive(Debug)]
struct PositiveInt(i32);
#[derive(Debug)]
struct NotPositiveError(i32);
impl fmt::Display for NotPositiveError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} is not positive", self.0)
}
}
impl TryFrom<i32> for PositiveInt {
type Error = NotPositiveError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value > 0 {
Ok(PositiveInt(value))
} else {
Err(NotPositiveError(value))
}
}
}
fn main() {
// Successful conversion
let pos = PositiveInt::try_from(42);
println!("From 42: {:?}", pos); // Ok(PositiveInt(42))
// Failed conversion
let neg = PositiveInt::try_from(-1);
println!("From -1: {:?}", neg); // Err(NotPositiveError(-1))
// Pattern matching on result
match PositiveInt::try_from(100) {
Ok(p) => println!("Created positive int: {:?}", p),
Err(e) => println!("Error: {}", e),
}
}
String Conversions#
String parsing and formatting are common conversion operations. Rust uses the
FromStr trait (called via .parse()) for parsing and ToString/Display
for formatting:
C++:
#include <iostream>
#include <sstream>
#include <string>
int main() {
// Number to string
int n = 42;
std::string s1 = std::to_string(n);
std::cout << "to_string: " << s1 << "\n";
// Using stringstream for formatting
std::ostringstream oss;
oss << "Value: " << n;
std::string s2 = oss.str();
std::cout << "stringstream: " << s2 << "\n";
// String to number
std::string num_str = "42";
int parsed = std::stoi(num_str);
std::cout << "Parsed: " << parsed << "\n";
// Error handling
try {
int bad = std::stoi("not a number");
} catch (const std::invalid_argument& e) {
std::cout << "Parse error: " << e.what() << "\n";
}
return 0;
}
Rust:
use std::str::FromStr;
fn main() {
// Number to String
let n = 42;
let s1: String = n.to_string();
let s2: String = format!("Value: {}", n);
println!("to_string: {}", s1);
println!("format!: {}", s2);
// String to number using parse()
let num_str = "42";
let parsed: i32 = num_str.parse().unwrap();
println!("Parsed: {}", parsed);
// With turbofish syntax
let parsed2 = "42".parse::<i32>().unwrap();
println!("Turbofish: {}", parsed2);
// Using FromStr directly
let parsed3 = i32::from_str("42").unwrap();
println!("FromStr: {}", parsed3);
// Error handling - parse returns Result
let bad: Result<i32, _> = "not a number".parse();
match bad {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Parse error: {}", e),
}
}
Deref Coercion#
Rust automatically applies deref coercion when a type implements the Deref
trait. This allows &String to be used where &str is expected, and
&Box<T> where &T is expected. This is similar to C++’s implicit
conversions but more controlled:
C++:
#include <iostream>
#include <memory>
#include <string>
void print_cstr(const char* s) {
std::cout << s << "\n";
}
void print_int_ref(const int& n) {
std::cout << n << "\n";
}
int main() {
std::string s = "hello";
// std::string implicitly converts to const char*
print_cstr(s.c_str()); // explicit .c_str() needed
// unique_ptr dereferences to T
auto ptr = std::make_unique<int>(42);
print_int_ref(*ptr); // explicit * needed
return 0;
}
Rust:
fn print_str(s: &str) {
println!("{}", s);
}
fn print_int_ref(n: &i32) {
println!("{}", n);
}
fn main() {
let s = String::from("hello");
// String automatically derefs to &str - no explicit conversion!
print_str(&s);
// Box<T> automatically derefs to &T
let boxed = Box::new(42);
print_int_ref(&boxed); // auto-deref, no * needed
// Works through multiple levels
let boxed_string = Box::new(String::from("nested"));
print_str(&boxed_string); // Box<String> -> String -> &str
// Vec<T> derefs to &[T]
fn print_slice(s: &[i32]) {
println!("{:?}", s);
}
let v = vec![1, 2, 3];
print_slice(&v); // Vec<i32> -> &[i32]
}
AsRef and AsMut#
AsRef and AsMut traits provide cheap reference-to-reference conversions.
They’re commonly used in generic functions to accept multiple types that can be
viewed as a reference to some target type:
C++:
#include <filesystem>
#include <iostream>
#include <string>
#include <string_view>
// Overloads for different string types
void print_path(const std::filesystem::path& p) {
std::cout << p << "\n";
}
void print_path(const std::string& s) {
print_path(std::filesystem::path(s));
}
void print_path(const char* s) {
print_path(std::filesystem::path(s));
}
int main() {
print_path("/tmp/file.txt");
print_path(std::string("/home/user"));
print_path(std::filesystem::path("/var/log"));
return 0;
}
Rust:
use std::path::Path;
// Single generic function accepts anything that can be viewed as &Path
fn print_path<P: AsRef<Path>>(path: P) {
println!("{}", path.as_ref().display());
}
fn main() {
// All of these work with the same function!
print_path("hello.txt"); // &str
print_path(String::from("hi.txt")); // String
print_path(Path::new("x.txt")); // &Path
print_path(std::path::PathBuf::from("y")); // PathBuf
// AsRef is also useful for byte slices
fn process_bytes<T: AsRef<[u8]>>(data: T) {
let bytes = data.as_ref();
println!("Got {} bytes", bytes.len());
}
process_bytes("hello"); // &str -> &[u8]
process_bytes(vec![1, 2, 3]); // Vec<u8> -> &[u8]
process_bytes(&[4, 5, 6][..]); // &[u8]
}
Unsafe Transmute#
std::mem::transmute reinterprets the bits of one type as another, similar to
C++’s reinterpret_cast. This is unsafe and should only be used when you’re
certain the bit patterns are valid for both types:
C++:
#include <cstdint>
#include <cstring>
#include <iostream>
int main() {
// reinterpret_cast for pointer types
uint32_t x = 0x12345678;
uint8_t* bytes = reinterpret_cast<uint8_t*>(&x);
std::cout << "Bytes: ";
for (int i = 0; i < 4; ++i) {
std::cout << std::hex << static_cast<int>(bytes[i]) << " ";
}
std::cout << "\n";
// memcpy for type punning (safer than reinterpret_cast)
float f = 3.14f;
uint32_t bits;
std::memcpy(&bits, &f, sizeof(f));
std::cout << "Float bits: 0x" << std::hex << bits << "\n";
return 0;
}
Rust:
fn main() {
// UNSAFE: transmute reinterprets bits
unsafe {
let x: u32 = 0x12345678;
let bytes: [u8; 4] = std::mem::transmute(x);
println!("Bytes: {:02x?}", bytes);
// Float to bits
let f: f32 = 3.14;
let bits: u32 = std::mem::transmute(f);
println!("Float bits: {:#x}", bits);
}
// Safe alternatives exist for common cases:
let x: u32 = 0x12345678;
let bytes = x.to_ne_bytes(); // native endian
println!("Safe bytes: {:02x?}", bytes);
let f: f32 = 3.14;
let bits = f.to_bits(); // safe float-to-bits
println!("Safe float bits: {:#x}", bits);
}
See Also#
Traits and Generics - From/Into as traits
Error Handling - TryFrom returns Result