Object Safety (dyn-Compatibility)

Level 4 — Error Handling & Generics The rules determining whether a trait can be used to form a Trait Object (dyn Trait).


1. Prerequisites

  • Trait Objects (dyn Trait) — What object safety governs the formation of.
  • Generics (<T>) — Contrasted against; generic methods are precisely what break object safety.
  • Sized Trait — The implicit bound every generic type parameter carries, which is part of why Self-returning methods are the problem.

2. Term Category

Type-System Rule (the vtable eligibility check): Not every trait can be turned into a dyn Trait. Object safety is the specific, checkable rule set the compiler applies to decide whether a trait's methods can all be called through a vtable — and thus whether dyn Trait is even legal to write.


3. Explanation

(1) Design Motivation — "Why did we design this?"

A trait object (dyn Trait) works by storing a vtable — a fixed-size list of function pointers, one per method, that the compiler fills in for whichever concrete type was originally boxed. For this to work, every method's signature must be knowable without knowing the concrete type at the call site. Two things break that: a method returning Self (the vtable can't know how big the concrete Self is, since different implementors have different sizes), and a method with its own generic type parameters (the vtable would need infinitely many entries, one per possible generic instantiation). Object safety is exactly the set of rules that reject both cases at compile time, with a clear error, instead of allowing you to build a trait object whose vtable simply couldn't work.

(2) Reality Metaphor

Imagine a universal remote control that can operate any brand of TV through a single row of generic buttons (a vtable).

  • Object-safe methods are like "power," "volume up," "channel down" — universal actions with a fixed, predictable effect, regardless of which specific TV brand (concrete type) is plugged in behind the scenes.
  • A Self-returning method is like a hypothetical "clone this exact TV model and hand it to me" button — the remote has no idea how big or what shape the specific brand's clone would be, so it simply cannot offer that button in its universal, one-size-fits-all row.
  • A generic method is like a button labeled "do a thing, but you must first hand me a chip specifying which thing" — the remote would need an infinite number of physical buttons to cover every possible chip in advance, which is a physical impossibility, so the remote refuses to expose it at all.

(3) Rust Code Examples

Short Snippet (An Object-Safe Trait)

trait Shape {
    fn area(&self) -> f64; // Fine: fixed signature, no `Self` return, no generics.
}

struct Circle { radius: f64 }
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } }

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 2.0 })];
    for s in &shapes {
        println!("{}", s.area()); // Works — dyn Shape is object-safe.
    }
}

Fuller Example (Two Ways to Break Object Safety)

trait Broken {
    fn clone_self(&self) -> Self where Self: Sized; // Returns `Self` — normally forbidden!
    fn process<T>(&self, item: T);                   // Generic method — also forbidden!
}

// COMPILE ERROR if you tried: let x: Box<dyn Broken> = ...;
// "the trait `Broken` cannot be made into an object"

// THE FIX for the `Self`-returning method: opt it OUT of the vtable
// with `where Self: Sized`, which tells the compiler "skip this method
// when building a vtable; it's only callable on concrete, known-sized types."
trait Fixed {
    fn clone_self(&self) -> Self where Self: Sized; // Now excluded from the vtable, not an error.
    fn describe(&self) -> String;                    // This one CAN go in the vtable.
}

4. Common Mistakes & Pitfalls

Mistake 1: Misunderstanding Object Safety Scoping and Lifecycle Rules

The mistake: Assuming Object Safety instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.

Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.

Incorrect:

fn get_ref() -> &str {
    let s = String::from("object_safety_data");
    &s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}

Fix:

fn get_string() -> String {
    let s = String::from("object_safety_data");
    s // Ownership of the String is transferred directly to the caller
}

Mistake 2: Mutating Object Safety State Without Exclusive Ownership or mut Borrowing

The mistake: Attempting to mutate data associated with Object Safety through an immutable reference &T or without specifying mut in variable declarations.

Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.

Incorrect:

fn update_val(data: &i32) {
    // *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}

Fix:

fn update_val(data: &mut i32) {
    *data += 1; // Correct: exclusive mutable reference permits mutation
}

Mistake 3: Concurrent Access to Object Safety Across Threads Without Send / Sync Guards

The mistake: Sharing non-thread-safe Object Safety instances across OS threads via std::thread::spawn.

Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).

Incorrect:

use std::rc::Rc;
use std::thread;

let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely

Fix:

use std::sync::Arc;
use std::thread;

let arc = Arc::new(42);
thread::spawn(move || {
    println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});

5. Practice Exercises

Exercise 1: Designing an Object-Safe Dynamic Plugin Registry with where Self: Sized Opt-Outs

Scenario: You are building an extensible text-processing engine for an enterprise platform. The core architecture relies on dynamic plugins stored in a plugin registry PluginRegistry holding heterogeneous components (Vec<Box<dyn Plugin>>).

The trait requirements present an object-safety challenge:

  1. Core object-safe methods: fn id(&self) -> &'static str and fn transform(&self, input: &str) -> String.
  2. Dynamic trait-object cloning: An object-safe helper method fn clone_box(&self) -> Box<dyn Plugin> so that Box<dyn Plugin> can implement Clone.
  3. Non-object-safe convenience methods: A constructor factory method fn create_default() -> Self and a generic batch execution method fn transform_batch<I: IntoIterator<Item = String>>(&self, items: I) -> Vec<String>.

Without where Self: Sized constraints on methods (2) and (3), methods returning Self or using generic type parameters break object safety (E0038), preventing the instantiation of dyn Plugin.

Task:

  1. Define the Plugin trait with object-safe transform methods, a clone_box helper, and opt-out annotations (where Self: Sized) on non-object-safe methods.
  2. Implement Clone for Box<dyn Plugin> using clone_box.
  3. Implement Plugin for two concrete structs: TextCleaner (trims whitespace and converts text to lowercase) and PrefixAppender (prefixes strings with a designated tag).
  4. Build PluginRegistry to manage Vec<Box<dyn Plugin>>, support cloning the entire registry, and execute input through all plugins sequentially.
  5. Write unit tests verifying dynamic execution, registry cloning, and concrete batch processing with explicit assertions (assert_eq!, assert!, assert_ne!, matches!).
Answer

Implementation

pub trait Plugin {
    fn id(&self) -> &'static str;
    fn transform(&self, input: &str) -> String;

    /// Object-safe helper method for cloning boxed trait objects.
    fn clone_box(&self) -> Box<dyn Plugin>;

    /// Excluded from vtable generation via `where Self: Sized`.
    fn create_default() -> Self
    where
        Self: Sized;

    /// Excluded from vtable generation via `where Self: Sized`.
    fn transform_batch<I: IntoIterator<Item = String>>(&self, items: I) -> Vec<String>
    where
        Self: Sized,
    {
        items.into_iter().map(|item| self.transform(&item)).collect()
    }
}

impl Clone for Box<dyn Plugin> {
    fn clone(&self) -> Self {
        self.clone_box()
    }
}

#[derive(Clone)]
pub struct TextCleaner;

impl Plugin for TextCleaner {
    fn id(&self) -> &'static str {
        "text_cleaner"
    }

    fn transform(&self, input: &str) -> String {
        input.trim().to_lowercase()
    }

    fn clone_box(&self) -> Box<dyn Plugin> {
        Box::new(self.clone())
    }

    fn create_default() -> Self {
        TextCleaner
    }
}

#[derive(Clone)]
pub struct PrefixAppender {
    pub prefix: String,
}

impl Plugin for PrefixAppender {
    fn id(&self) -> &'static str {
        "prefix_appender"
    }

    fn transform(&self, input: &str) -> String {
        format!("{}: {}", self.prefix, input)
    }

    fn clone_box(&self) -> Box<dyn Plugin> {
        Box::new(self.clone())
    }

    fn create_default() -> Self {
        PrefixAppender {
            prefix: "LOG".to_string(),
        }
    }
}

#[derive(Clone, Default)]
pub struct PluginRegistry {
    plugins: Vec<Box<dyn Plugin>>,
}

impl PluginRegistry {
    pub fn new() -> Self {
        Self { plugins: Vec::new() }
    }

    pub fn register(&mut self, plugin: Box<dyn Plugin>) {
        self.plugins.push(plugin);
    }

    pub fn execute_all(&self, input: &str) -> String {
        let mut result = input.to_string();
        for plugin in &self.plugins {
            result = plugin.transform(&result);
        }
        result
    }

    pub fn len(&self) -> usize {
        self.plugins.len()
    }

    pub fn is_empty(&self) -> bool {
        self.plugins.is_empty()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_dyn_plugin_registry_execution() {
        let mut registry = PluginRegistry::new();
        registry.register(Box::new(TextCleaner::create_default()));
        registry.register(Box::new(PrefixAppender {
            prefix: "INFO".to_string(),
        }));

        assert_eq!(registry.len(), 2);
        assert!(!registry.is_empty());

        let raw = "   HELLO RUST WORLD   ";
        let processed = registry.execute_all(raw);
        assert_eq!(processed, "INFO: hello rust world");
    }

    #[test]
    fn test_registry_cloning() {
        let mut registry = PluginRegistry::new();
        registry.register(Box::new(TextCleaner));

        let cloned_registry = registry.clone();
        assert_eq!(registry.len(), cloned_registry.len());

        let res1 = registry.execute_all(" TEST ");
        let res2 = cloned_registry.execute_all(" TEST ");
        assert_eq!(res1, res2);
        assert_eq!(res1, "test");
    }

    #[test]
    fn test_sized_batch_method() {
        let cleaner = TextCleaner::create_default();
        let batch = vec!["  FOO  ".to_string(), "  BAR  ".to_string()];
        let results = cleaner.transform_batch(batch);
        assert_eq!(results, vec!["foo".to_string(), "bar".to_string()]);
        assert_ne!(results[0], "  FOO  ");
    }
}

Technical Explanation

  1. Object-Safety Rules & Vtable Construction: A trait can only be turned into a trait object (dyn Trait) if all vtable function pointers have fixed, type-erased memory signatures. Returning Self (which requires knowing the concrete size of Self at compile time) or declaring generic parameters <I> (which requires monomorphizing an infinite number of vtable slots) violates object safety (E0038).
  2. Opt-Out Mechanism via where Self: Sized: Adding where Self: Sized to create_default() and transform_batch() explicitly informs the compiler that these methods are only available when Self has a static, known size. Because dyn Plugin is dynamically sized (?Sized), dyn Plugin does not fulfill Self: Sized. The compiler excludes these methods from vtable generation, preserving object safety for the rest of the trait.
  3. Trait-Object Cloning Pattern: std::clone::Clone requires Self: Sized on fn clone(&self) -> Self, making Clone non-object-safe. By implementing a custom clone_box(&self) -> Box<dyn Plugin> method on the trait, we delegate concrete cloning to the underlying type while returning a fat pointer (Box<dyn Plugin>). Implementing Clone for Box<dyn Plugin> directly invokes clone_box().
  4. Monomorphization vs Dynamic Dispatch: Invoking transform() on &dyn Plugin performs dynamic dispatch by dereferencing the vtable pointer. Invoking transform_batch() on a concrete TextCleaner uses static monomorphization at compile time without any vtable indirection overhead.

Exercise 2: Refactoring Generic Parameters to Trait Objects for Event Processing Pipelines

Scenario: In a streaming telemetry system, an event processing pipeline filters and transforms telemetry frames. An initial attempt at defining EventFilter used generic parameters: fn filter<T: std::fmt::Display>(&self, payload: &str, context: T) -> Option<String>; This trait breaks object safety because the compiler cannot construct a static vtable layout for generic methods.

Task:

  1. Refactor EventFilter to be object-safe by converting generic parameter T into a trait object reference &dyn std::fmt::Display.
  2. Add object-safe methods: fn id(&self) -> &str and fn process(&self, payload: &str, tag: &dyn std::fmt::Display) -> Option<String>.
  3. Implement EventFilter for two filters:
    • MinLengthFilter: Drops payloads shorter than min_len (returns None).
    • SensitiveWordMasker: Replaces target forbidden strings with "[REDACTED]".
  4. Create StreamPipeline holding Vec<Box<dyn EventFilter>>. Implement run_pipeline(&self, payload: &str, tag: &dyn Display) -> Option<String> which processes input sequentially and short-circuits if any filter returns None.
  5. Write unit tests with explicit assertions (assert_eq!, assert!, assert_ne!, matches!) covering successful pipeline execution, drop conditions, and parameter polymorphism.
Answer

Implementation

use std::fmt::Display;

pub trait EventFilter {
    fn id(&self) -> &str;
    /// Object-safe method taking a dynamic trait object reference `&dyn Display`
    /// instead of a generic parameter `<T: Display>`.
    fn process(&self, payload: &str, tag: &dyn Display) -> Option<String>;
}

pub struct MinLengthFilter {
    pub min_len: usize,
}

impl EventFilter for MinLengthFilter {
    fn id(&self) -> &str {
        "min_length_filter"
    }

    fn process(&self, payload: &str, tag: &dyn Display) -> Option<String> {
        let tag_str = tag.to_string();
        if payload.len() >= self.min_len {
            Some(format!("[{}] {}", tag_str, payload))
        } else {
            None
        }
    }
}

pub struct SensitiveWordMasker {
    pub word_to_mask: String,
}

impl EventFilter for SensitiveWordMasker {
    fn id(&self) -> &str {
        "sensitive_word_masker"
    }

    fn process(&self, payload: &str, _tag: &dyn Display) -> Option<String> {
        let masked = payload.replace(&self.word_to_mask, "[REDACTED]");
        Some(masked)
    }
}

pub struct StreamPipeline {
    filters: Vec<Box<dyn EventFilter>>,
}

impl StreamPipeline {
    pub fn new() -> Self {
        Self { filters: Vec::new() }
    }

    pub fn add_filter(&mut self, filter: Box<dyn EventFilter>) {
        self.filters.push(filter);
    }

    pub fn run_pipeline(&self, initial_payload: &str, tag: &dyn Display) -> Option<String> {
        let mut current = initial_payload.to_string();
        for filter in &self.filters {
            match filter.process(&current, tag) {
                Some(next_payload) => current = next_payload,
                None => return None,
            }
        }
        Some(current)
    }

    pub fn filter_count(&self) -> usize {
        self.filters.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_stream_pipeline_success() {
        let mut pipeline = StreamPipeline::new();
        pipeline.add_filter(Box::new(MinLengthFilter { min_len: 5 }));
        pipeline.add_filter(Box::new(SensitiveWordMasker {
            word_to_mask: "CONFIDENTIAL".to_string(),
        }));

        let tag = 404; // Integer implementing Display
        let input = "System status: CONFIDENTIAL data processed";
        let result = pipeline.run_pipeline(input, &tag);

        assert!(result.is_some());
        let output = result.unwrap();
        assert!(output.contains("[REDACTED]"));
        assert!(output.starts_with("[404]"));
        assert_ne!(output, input);
    }

    #[test]
    fn test_stream_pipeline_filter_drop() {
        let mut pipeline = StreamPipeline::new();
        pipeline.add_filter(Box::new(MinLengthFilter { min_len: 20 }));

        let tag = "WARN";
        let input = "Short msg";
        let result = pipeline.run_pipeline(input, &tag);

        assert!(result.is_none());
        assert!(matches!(result, None));
    }

    #[test]
    fn test_trait_object_param_flexibility() {
        let masker = SensitiveWordMasker {
            word_to_mask: "BAD".to_string(),
        };
        let res1 = masker.process("BAD message", &"STR_TAG");
        let res2 = masker.process("BAD message", &99);

        assert_eq!(res1.unwrap(), "[REDACTED] message");
        assert_eq!(res2.unwrap(), "[REDACTED] message");
    }
}

Technical Explanation

  1. Why Generic Methods Invalidate Vtables: If a method is generic (fn filter<T: Display>), monomorphization generates a distinct compiled function address for every type T instantiated across the entire codebase. A vtable is a static struct of function pointers created at type definition time. Because the compiler cannot anticipate all possible types T at the point of vtable construction, generic methods break object safety.
  2. Refactoring to Trait Objects (&dyn Display): By converting the parameter from generic type T to fat pointer &dyn Display, the method signature becomes uniform across all invocation call sites. The function pointer in the EventFilter vtable accepts a 2-word fat pointer (data pointer + vtable pointer for Display).
  3. Fat Pointer Composition: When calling process(&current, &404), Rust automatically coerces the reference &i32 into &dyn Display at the call site. The vtable for EventFilter calls the method, which in turn performs a second vtable lookup when invoking tag.to_string().
  4. Trade-offs (Static vs Dynamic Dispatch):
    • Static Dispatch (Generics): Zero-cost abstraction, aggressive inlining, but monomorphization bloat and non-object-safe.
    • Dynamic Dispatch (&dyn Trait): Trait-object compatible, heterogeneous collection support (Vec<Box<dyn EventFilter>>), but introduces pointer indirection and prevents compiler inlining.

Exercise 3: Trait Object Safety in Heterogeneous Error Hierarchies & Dynamic Downcasting

Scenario: In an asynchronous microservice framework, tasks execute across isolated subsystems (database access, network I/O). To propagate heterogeneous errors across module boundaries, errors must be boxed as object-safe trait objects (Box<dyn std::error::Error + Send + Sync + 'static>).

Task:

  1. Define custom error structs: DatabaseError (fields: code: u32, table: String) and NetworkError (fields: endpoint: String, timeout_ms: u64).
  2. Implement std::fmt::Display and std::error::Error for both structs.
  3. Define an object-safe trait ServiceTask with method fn execute(&self) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>>.
  4. Implement ServiceTask for DbTask and NetTask.
  5. Implement a categorization function categorize_error(err: &(dyn std::error::Error + 'static)) -> ErrorCategory using Any::downcast_ref to inspect underlying concrete errors without breaking type erasure.
  6. Write unit tests with explicit assertions (assert_eq!, assert!, assert_ne!, matches!) for error generation, formatting, dynamic downcasting, and pattern matching.
Answer

Implementation

use std::error::Error;
use std::fmt;

#[derive(Debug, PartialEq, Eq)]
pub struct DatabaseError {
    pub code: u32,
    pub table: String,
}

impl fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Database error [code {}] on table '{}'", self.code, self.table)
    }
}

impl Error for DatabaseError {}

#[derive(Debug, PartialEq, Eq)]
pub struct NetworkError {
    pub endpoint: String,
    pub timeout_ms: u64,
}

impl fmt::Display for NetworkError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Network connection to '{}' timed out after {} ms",
            self.endpoint, self.timeout_ms
        )
    }
}

impl Error for NetworkError {}

pub type TaskResult = Result<String, Box<dyn Error + Send + Sync + 'static>>;

pub trait ServiceTask {
    fn name(&self) -> &str;
    fn execute(&self) -> TaskResult;
}

pub struct DbTask {
    pub table: String,
    pub should_fail: bool,
}

impl ServiceTask for DbTask {
    fn name(&self) -> &str {
        "db_task"
    }

    fn execute(&self) -> TaskResult {
        if self.should_fail {
            Err(Box::new(DatabaseError {
                code: 1045,
                table: self.table.clone(),
            }))
        } else {
            Ok("DB sync complete".to_string())
        }
    }
}

pub struct NetTask {
    pub endpoint: String,
    pub should_fail: bool,
}

impl ServiceTask for NetTask {
    fn name(&self) -> &str {
        "net_task"
    }

    fn execute(&self) -> TaskResult {
        if self.should_fail {
            Err(Box::new(NetworkError {
                endpoint: self.endpoint.clone(),
                timeout_ms: 3000,
            }))
        } else {
            Ok("Network payload sent".to_string())
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum ErrorCategory {
    Database { code: u32 },
    Network { timeout_ms: u64 },
    Unknown,
}

pub fn categorize_error(err: &(dyn Error + 'static)) -> ErrorCategory {
    if let Some(db_err) = err.downcast_ref::<DatabaseError>() {
        ErrorCategory::Database { code: db_err.code }
    } else if let Some(net_err) = err.downcast_ref::<NetworkError>() {
        ErrorCategory::Network {
            timeout_ms: net_err.timeout_ms,
        }
    } else {
        ErrorCategory::Unknown
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_successful_task_execution() {
        let db_task = DbTask {
            table: "orders".to_string(),
            should_fail: false,
        };
        let res = db_task.execute();
        assert!(res.is_ok());
        assert_eq!(res.unwrap(), "DB sync complete");
    }

    #[test]
    fn test_error_boxing_and_downcasting_database() {
        let db_task = DbTask {
            table: "users".to_string(),
            should_fail: true,
        };
        let res = db_task.execute();
        assert!(res.is_err());

        let boxed_err = res.unwrap_err();
        let display_str = boxed_err.to_string();
        assert_eq!(display_str, "Database error [code 1045] on table 'users'");

        let category = categorize_error(boxed_err.as_ref());
        assert_eq!(category, ErrorCategory::Database { code: 1045 });
        assert_ne!(category, ErrorCategory::Unknown);
    }

    #[test]
    fn test_error_boxing_and_downcasting_network() {
        let net_task = NetTask {
            endpoint: "https://api.service.com".to_string(),
            should_fail: true,
        };
        let res = net_task.execute();
        assert!(res.is_err());

        let boxed_err = res.unwrap_err();
        let category = categorize_error(boxed_err.as_ref());
        assert_eq!(category, ErrorCategory::Network { timeout_ms: 3000 });
        assert!(matches!(category, ErrorCategory::Network { timeout_ms: 3000 }));
    }
}

Technical Explanation

  1. Object Safety of std::error::Error: The std::error::Error trait is object-safe because all its methods (source(), description(), fmt()) receive &self and do not return Self or take generic parameters. This allows standard errors to be boxed into Box<dyn Error>.
  2. Lifetime Bounds ('static) for Downcasting: To inspect type erasure at runtime via err.downcast_ref::<T>(), the trait object must satisfy the 'static lifetime bound. This guarantees that the concrete type T contains no non-static references that could dangle after dynamic casting.
  3. Vtable Inspection via TypeId: err.downcast_ref::<DatabaseError>() queries the compiler-generated TypeId associated with the trait object's underlying concrete type inside the vtable. If TypeId::of::<DatabaseError>() matches the dynamic object's TypeId, the compiler safely casts the internal data pointer *const () to &DatabaseError.
  4. Thread-Safety Traits (Send + Sync): Adding auto trait bounds + Send + Sync to dyn Error restricts boxed trait objects to thread-safe types, enabling safe transfer across async task executors (tokio::spawn, std::thread::spawn) without altering the structure of the vtable.

  • Trait Objects (dyn Trait) — What object safety is a precondition for.
  • Associated Constants — Another feature that, if present on a trait, breaks object safety.
  • Fat Pointers (Wide Pointers) — The underlying dyn Trait representation (data pointer + vtable pointer) that object safety exists to keep well-formed.
  • Sized Traitwhere Self: Sized is the standard escape hatch to exclude a specific method from the vtable requirement.

7. Key Takeaways

  • Object safety is the rule set determining whether dyn Trait can legally be formed for a given trait.
  • The two classic violations: a method returning Self by value, and a method with its own generic type parameters.
  • Associated constants also break object safety (no per-instance slot for them in a vtable).
  • where Self: Sized on an individual method excludes just that method from the vtable requirement, letting the rest of the trait remain object-safe.
Built with LogoFlowershow