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.

struct Person { name: String, } impl AsRef<str> for Person { fn as_ref(&self) -> &str { &self.name } } fn process<T: AsRef<str>>(name: T) { println!("{}", name.as_ref()); } fn main() { let alice = Person { name: "Alice".to_string(), }; process(alice); }

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

Check your understanding

1 question