Generics allow writing code that is independent of a specific type. They are implemented using angle bracket syntax:
fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
Here, T is a generic type constrained by the PartialOrd trait.
Generic parameters are declared using <T>, but they can be constrained using trait bounds with a colon, for example, <T: Display>. This communicates to the compiler that only types for which the needed trait is implemented can be used.
In Rust, there are two forms of dispatch for generics:
dyn Trait is used, the call is made through a virtual table (vtable).Impact on machine code: Using generics with trait bounds (without dyn Trait) leads to monomorphization: an increase in the binary size but maximum speed. Using dyn Trait saves on the binary size but incurs a performance hit.
Question: There's a function
fn do_something<T: Debug>(value: &T)
Will the compiler create a separate do_something function in the binary code for each type with which it is used, or will it use a universal implementation?
Typical incorrect answer: It will use one function for all types thanks to the trait bound.
Correct answer: The compiler creates separate copies of this function for each type (monomorphization), since the trait bound does not make the generic function "universal" via vtable. Universality only appears with dyn Trait (dynamic dispatch).
Example:
fn print_val<T: std::fmt::Debug>(val: T) { println!("{:?}", val); } // A separate version of the function will be created for each call with a different type
Story
In a project with large generic objects, it was found that the binary file became significantly larger than expected. Later it turned out: the reason was in the widespread use of generic functions without constraints. Calls with dozens of types led to exponential growth of the executable file size (code bloat), which was revealed only during the release build on CI.
Story
One of the developers accepted a generic parameter with a trait bound, believing that such code worked with "dynamic" dispatch. This led to excessive memory consumption on the server and reduced performance due to the constant growth of code and its caching by the processor.
Story
In the library, they attempted to use a generic trait with a Self type (for example, trait Clone) as
dyn Trait, which is not supported in Rust and resulted in a compilation error. The interface needed to be rewritten explicitly; otherwise, the generic API would not work in dynamic mode, and the interface would have to be changed at compile-time.