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.


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