Rust Traits vs C++ Interfaces: Generics and Dispatch¶
A Rust trait is not a class containing inherited fields, and a C++ concept is not a virtual base class. Start with the required behaviour, then separately choose whether a caller knows the concrete type, whether different types share a collection, and who owns their storage.
Requirement: read through one behavioural contract¶
After borrowing and const, review traits and dispatch. Use Rust edition 2024 and C++20, with no external libraries.
A source returns a measured integer or absence. The caller must preserve zero and negative values, support a concrete source in a generic function, and aggregate a borrowed mixture of source types without taking ownership. The concrete types should not need identical fields. This is a fixture policy, not a claim about a physical sensor or an error-reporting protocol; absence is not a substitute for an I/O error.
C++: a concept and a virtual interface solve different problems¶
Save this complete program as main.cpp:
Compile/run with g++ -std=c++20 -Wall -Wextra -Wpedantic -O0 main.cpp -o dispatch_debug, then repeat with -O2 and a different executable name. Keep assertions enabled; do not add -DNDEBUG.
Readable checks an expression requirement for template arguments. Fixed and Missing satisfy it without inheriting from Source. Adapter is an explicit composition bridge to a virtual interface for the mixed collection. It is not a requirement for every C++ design; a source could implement that interface directly when appropriate.
The C++ constraint rules govern template acceptance, while virtual functions provide dynamic calls through a base interface. Neither declaration proves the behavioural meaning of absence or a reading's units. All pointers in this fixture are non-null and refer to live stack objects; the function assumes that contract. These pointers do not own the objects.
Rust: one trait can serve generic and erased callers¶
Create a package:
Replace src/main.rs with this complete program:
Run cargo check, cargo fmt --check, cargo test and cargo test --release, then compare cargo run --quiet with cargo run --release --quiet. Four tests should pass. Both programs print:
The trait declaration specifies behaviour, not a shared base-class data layout. Implementations attach it to each concrete type. An unrelated type with a method named read does not automatically implement Source. Traits can also have associated types/constants and default methods, but this trait stays small and dyn-compatible.
The trait-object contract permits the borrowed mixed collection to call implementations dynamically. &dyn Source is not a heap allocation and does not transfer ownership. Choosing Box
The sum is intentionally exercised with three small fixtures. General collection sizes and arbitrary values require an explicit overflow policy; dispatch does not supply one.
Change the requirement: choose either concrete type at runtime¶
make_fixed hides one concrete return type in Rust. Return-position impl Trait is not a dynamic union of every implementation. A runtime branch returning Fixed on one path and Missing on another does not establish one hidden concrete type.
Save the normal source and temporarily replace it with this invalid program:
cargo check rejects incompatible branch types with E0308 on the verified compiler. A C++ deduced auto return also does not combine unrelated return types. Compile this independent failure.cpp:
For runtime selection, choose a concrete enum/std::variant if the set is closed, or an owning erased interface if callers need independently owned values from an open implementation set. For a Rust Box
Selection criteria and exercises¶
| Requirement | Candidate | Remaining obligation |
|---|---|---|
| Concrete type selected by the caller | Generic trait bound / constrained template | Specify semantic requirements beyond the signatures |
| Borrowed heterogeneous sources | &dyn Trait / virtual base reference or pointer | Preserve ownership/lifetime; C++ pointers must also meet the non-null contract here |
| Hide one concrete result | impl Trait / deduced auto return | Each return path must produce its required concrete type |
| Closed alternatives selected at runtime | Enum / variant | Handle each state and preserve its data |
| Independently owned open alternatives | Box |
Define allocation, failure and destruction contracts |
Rust coherence limits which trait implementations can exist; C++ template specialisation and overload selection have their own rules. A supertrait is a requirement to implement another trait, not inherited member storage. Const generics are not a direct replacement for every C++ template or compile-time facility. Revisit the generics and trait implementation lessons before treating similarly named mechanisms as equivalent.
Exercises:
- Add a new source with a different field layout, then include it in the mixed collection. The report must count it without copying the original owners.
- Remove the Rust implementation but leave an inherent read method. A Source-bound call must fail: method spelling alone is insufficient.
- Repair the failing return with an owning interface, then test both present and missing selections. Explain why this changes the ownership contract from the earlier borrowed array.
- Replace the open source family with a closed enum/variant when requirements fix the alternatives. Identify which code changes when adding a variant versus adding an operation.
- Explain why a virtual call does not require heap allocation, and why no timing or code-size claim follows from these tests.
Verification and next step¶
On October 10, 2026, the authorised Raspberry Pi 4B verified the extracted sources with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust/Cargo 1.99.0 and GCC g++ 14.2.0. Rust check/formatting, four debug/release tests and exact outputs passed. The additional-source variant passed five tests; the owned Box
Next planned: error results, C++ exceptions and panic boundaries. Follow the published links in the course overview.