User-defined conversions
In C++ user-defined conversions are created using converting
constructors
or conversion
functions. Because
converting constructors are opt-out (via the explicit
specifier), implicit
conversions occur with regularity in C++ code. In the following example both the
assignments and the function calls make use of implicit conversions as provided
by a converting constructor.
Rust makes significantly less use of implicit conversions. Instead most
conversions are explicit. The
std::convert
module
provides several traits for working with user-defined conversions. In Rust, the
below example makes use of explicit conversions by implementing the From
trait.
struct Widget {
Widget(int) {}
Widget(int, int) {}
};
void process(Widget w) {}
int main() {
Widget w1 = 1;
Widget w2 = {4, 5};
process(1);
process({4, 5});
return 0;
}
struct Widget;
impl From<i32> for Widget {
fn from(_x: i32) -> Widget {
Widget
}
}
impl From<(i32, i32)> for Widget {
fn from(_x: (i32, i32)) -> Widget {
Widget
}
}
fn process(w: Widget) {}
fn main() {
let w1: Widget = 1.into();
// For construction this is more idiomatic:
let w1b = Widget::from(1);
let w2: Widget = (4, 5).into();
// For construction this is more idiomatic:
let w2b = Widget::from((4, 5));
process(1.into());
process((4, 5).into());
}
The into
method used above is provided via a blanket
implementations
for the Into trait
for types that implement the From
trait. Because of the existence of the
blanket
implementation,
it is generally preferred to implement the From
trait instead of the Into
trait, and let the Into
trait be provided by that blanket implementation.
Conversion functions
C++ conversion functions enable conversions in the other direction, from the defined class to another type.
To achieve the same in Rust, the From
trait can be implemented in the other
direction. At least one of the source type or the target type must be defined in
the same crate as the trait implementation.
#include <utility>
struct Point {
int x;
int y;
operator std::pair<int, int>() const {
return std::pair(x, y);
}
};
void process(std::pair<int, int>) {}
int main() {
Point p1{1, 2};
Point p2{3, 4};
std::pair<int, int> xy = p1;
process(p2);
return 0;
}
struct Point {
x: i32,
y: i32,
}
impl From<Point> for (i32, i32) {
fn from(p: Point) -> (i32, i32) {
(p.x, p.y)
}
}
fn process(x: (i32, i32)) {}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let xy: (i32, i32) = p1.into();
process(p2.into());
}
Conversion functions are is often used to implement the safe bool pattern in C++, which is addressed in a different way in Rust.
Borrowing conversions
The methods in the From
and Into
traits take ownership of the values to be
converted. When this is not desired in C++, the conversion function can just
take and return references.
To achieve the same in Rust the AsRef
trait or AsMut
trait are used.
#include <iostream>
#include <string>
struct Person {
std::string name;
operator std::string &() {
return this->name;
}
};
void process(const std::string &name) {
std::cout << name << std::endl;
}
int main() {
Person alice{"Alice"};
process(alice);
return 0;
}
struct Person {
name: String,
}
impl AsRef<str> for Person {
fn as_ref(&self) -> &str {
&self.name
}
}
fn process(name: &str) {
println!("{}", name);
}
fn main() {
let alice = Person {
name: "Alice".to_string(),
};
process(alice.as_ref());
}
It is common to use AsRef
or AsMut
as a trait bound in function definitions.
Using generics with an AsRef
or AsMut
bound allows clients to call the
functions with anything that can be cheaply viewed as the type that the function
wants to work with. Using this technique, the above definition of process
would be defined as in the following example.
This technique is often used with functions that take file system paths, so that literal strings can more easily be used as paths.
Fallible conversions
In C++ when conversions might fail it is possible (though usually discouraged) to throw an exception from the converting constructor or converting function.
Error handling in Rust does not use exceptions. Instead
the TryFrom
trait
and TryInto
trait
are used for fallible conversions. These traits differ from From
and Into
in
that they return a Result
, which may indicate a failing case. When a
conversion may fail one should implement TryFrom
and rely on the client to
call unwrap
on the result, rather than panic in a From
implementation.
#include <stdexcept>
#include <string>
class NonEmpty {
std::string s;
public:
NonEmpty(std::string s) : s(s) {
if (this->s.empty()) {
throw std::domain_error("empty string");
}
}
};
int main() {
std::string s("");
NonEmpty x = s; // throws
return 0;
}
use std::convert::TryFrom;
use std::convert::TryInto;
struct NonEmpty {
s: String,
}
#[derive(Clone, Copy, Debug)]
struct NonEmptyStringError;
impl TryFrom<String> for NonEmpty {
type Error = NonEmptyStringError;
fn try_from(
s: String,
) -> Result<NonEmpty, NonEmptyStringError>
{
if s.is_empty() {
Err(NonEmptyStringError)
} else {
Ok(NonEmpty { s })
}
}
}
fn main() {
let res: Result<
NonEmpty,
NonEmptyStringError,
> = "".to_string().try_into();
match res {
Ok(ne) => {
println!("Converted!");
}
Err(err) => {
println!("Couldn't convert");
}
}
}
Just like with From
and Into
, there is a blanket
implementation
for TryInto
for everything that implements TryFrom
.
Implicit conversions
Rust does have one kind of user-defined implicit conversion, called deref
coercions,
provided by the Deref
trait and
DerefMut
trait. These
coercions exist for making pointer-like types more ergonomic to use.
An example of implementing the traits for a custom pointer-like type is given in the Rust book.
Summary
A summary of when to use which kind of conversion interface is given in the
documentation for the std::convert
module.