RTTI and dynamic_cast
Rust does not have built-in support for generalized RTTI, nor does Rust have a direct analog to
dynamic_cast
.
The only language primitive provided by Rust in this vein is TypeId
,
which is a globally unique identifier for a type. Rust's standard library builds on TypeId
to provide an Any
trait that supports similar
uses to std::any
in C++. However, Any
does not enable testing for
implementation of, or converting to, another trait. It only enables testing for
and converting to a specific type.
Every type with a 'static
lifetime bound (i.e., that does not contain
references with a non-static lifetime) implements Any
via a blanket implementation in
the standard library.
#include <any>
#include <iostream>
#include <string>
void print_if_string(const std::any &x) {
try {
const std::string &s =
any_cast<std::string const &>(x);
std::cout << s << std::endl;
} catch (std::bad_any_cast &e) {
std::cout << "Not a string!" << std::endl;
}
}
int main() {
print_if_string(std::string("hello world"));
print_if_string(5);
}
use std::any::Any; fn print_if_string(x: &dyn Any) { match x.downcast_ref::<String>() { Some(s) => println!("{}", s), None => println!("Not a string!") } } fn main() { print_if_string(&String::from("hello world")); print_if_string(&5); }
Event handling
One practical use of RTTI and dynamic_cast
in C++ is for event handling in
situations where both the subsystem generating events and the events themselves
need to be decoupled from the handling logic. This is usually because the events
are generated by a framework, such as a GUI or game framework, while the
response to the events is application-specific.
struct Event {
virtual ~Event() = default;
};
struct ClickEvent : public Event {
int x;
int y;
};
struct ResizeEvent : public Event {
int old_height;
int old_width;
int new_height;
int new_width;
};
void handle_event(Event *e) {
if (auto click_event =
dynamic_cast<ClickEvent *>(e)) {
// ...
} else if (auto resize_event =
dynamic_cast<ResizeEvent *>(e)) {
// ...
} else {
// ... handle unknown event ...
}
}
// register event handler in main
#![allow(unused)] fn main() { enum Event { ClickEvent { x: i32, y: i32, }, ResizeEvent { old_height: i32, old_width: i32, new_height: i32, new_width: i32, }, } fn handle_event(e: Event) { match e { Event::ClickEvent { x, y } => { // ... } Event::ResizeEvent { old_height, old_width, new_height, new_width, } => { // ... } } } }
Even when the a client of the library is needs to be able to define custom events, it is usually possible to make use of an event enum. This is the approach taken by the winit crate, which does cross-platform window and event loop management.
struct Event {
virtual ~Event() = default;
};
struct ClickEvent : public Event {
int x;
int y;
};
struct ResizeEvent : public Event {
int old_height;
int old_width;
int new_height;
int new_width;
};
struct DoSomething : public Event {
double how_much;
}
struct DoSomethingElse : public Event {
double how_many;
}
void handle_event(Event *e) {
if (auto click_event =
dynamic_cast<ClickEvent *>(e)) {
// ...
} else if (auto resize_event =
dynamic_cast<ResizeEvent *>(e)) {
// ...
// ...
} else if (auto user_event =
dynamic_cast<DoSomething *>(e)) {
// ...
} else if (auto user_event =
dynamic_cast<DoSomethingElse *>(
e)) {
// ...
} else {
// ... handle unknown event ...
}
}
#![allow(unused)] fn main() { enum Event<T> { ClickEvent { x: i32, y: i32, }, ResizeEvent { old_height: i32, old_width: i32, new_height: i32, new_width: i32, }, // ... UserEvent(T), } enum UserEvent { DoSomething { how_much: f64 }, DoSomethingElse { how_many: i32 }, } fn handle_event(e: Event<UserEvent>) { match e { Event::ClickEvent { x, y } => { // ... } Event::ResizeEvent { old_height, old_width, new_height, new_width, } => { // ... } // ... Event::UserEvent( UserEvent::DoSomething { how_much }, ) => { // ... } Event::UserEvent( UserEvent::DoSomethingElse { how_many, }, ) => { // ... } } } }
When representing events as an enum truly isn't feasible, sometimes double
dispatch can be used instead. Otherwise it may be
necessary to use the Any
trait or to define an Event
trait that exposes a
type identifier that an be used for safe downcasting (via Any
) or unsafe
downcasting behind a safe interface.1
Library support for reflection via macros
Some of the use cases of RTTI can be achieved in Rust by using one of the third-party reflection libraries. These libraries implement reflection by providing macros for deriving traits to support common reflection operations. Rust reflection libraries include bevy_reflect, facet, and mirror-mirror.
The derive-macro approach to reflection essentially makes it opt-in, so that software that does not use reflection does not have to pay a price for it (performance costs or binary size). However, due to Rust's orphan rule, this approach makes it more difficult to integrate third-party types that lack the derived trait.
-
Such an interface usually involves providing individual event handling functions for specific types, rather than a single large event handling function, so that the underlying implementation can managing the enforcement of the invariants required to make the casting safe. ↩