Monomorphization
Monomorphization
Level 4 — Error Handling & Generics The compiler generates specialized code for each concrete type used with generics — zero-cost abstraction.
1. Prerequisites
- Generics (
<T>) — The feature that triggers this compiler mechanism. - fn — The primary place where this code duplication happens.
2. Term Category
Rust-specific (the compiler magic): "Monomorphization" is a massive, scary word for a very simple concept: it is the exact physical mechanism the Rust compiler uses to implement Generics so that they run at blazing speeds.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In languages like Java or Python, a generic function usually works by passing everything around as a generic object or pointer under the hood. Because the compiled code doesn't know exactly what type it's dealing with, the CPU has to do extra work at runtime to figure out how to handle the data (this is called dynamic dispatch). This makes generic code inherently slightly slower than hard-coding a specific function.
Rust's core philosophy is "Zero-Cost Abstractions". Rust wants you to be able to use elegant abstractions (like Generics) without losing a single drop of performance.
Rust achieves this via Monomorphization. When you write a generic function, the Rust compiler looks at everywhere you called it. If you called it with an i32 and an f64, the compiler secretly deletes your generic function, and automatically copy-pastes two brand new, hard-coded functions into the final binary. At runtime, there is no generic code at all!
(2) Reality Metaphor
Imagine you write a generic recipe for "Baking a <T>".
If you give it to a Java chef, the chef keeps the generic recipe on the wall. When you order a cake, the chef has to stop, read the generic recipe, translate <T> to "Cake" in their head, figure out how a cake behaves, and then bake it. This translation takes time.
If you give it to the Rust compiler, the compiler looks at your restaurant, sees that you only ever serve "Cake" and "Pie", and secretly throws away your generic recipe. Instead, it prints out two brand new, specific recipes: "Baking a Cake" and "Baking a Pie". When the chef cooks, there is zero translation time. They just read the hard-coded recipe and go perfectly fast.
(3) Rust Code Examples
Short Snippet (What you write vs What Rust compiles)
// 1. What you write:
fn print_item<T>(item: T) {
// ... logic ...
}
fn main() {
print_item(5); // Calling with i32
print_item("Hello"); // Calling with &str
}
// 2. What the compiler ACTUALLY turns it into (Monomorphization):
// Notice the generic <T> is completely gone!
fn print_item_i32(item: i32) {
// ... logic ...
}
fn print_item_str(item: &str) {
// ... logic ...
}
fn main() {
print_item_i32(5);
print_item_str("Hello");
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Monomorphization Scoping and Lifecycle Rules
The mistake: Assuming Monomorphization 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("monomorphization_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("monomorphization_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Monomorphization State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Monomorphization 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 Monomorphization Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Monomorphization 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: Mitigating Monomorphization Binary Bloat via Inner Non-Generic Helpers
Scenario: In high-throughput packet processing engines, generic functions often handle common operations like header construction, sequence stamping, checksum calculation, and slice bounds checks. When encode_packet<P: PacketPayload>(&mut self, payload: &P, out: &mut [u8]) is called for dozens of packet payload types, the Rust compiler monomorphizes the entire function body for every type P, creating duplicate machine instructions that inflate binary size and trigger CPU instruction cache (I-cache) misses.
Refactor a generic packet encoder using the outline pattern (inner non-generic helper function pattern). Extract all payload-agnostic logic (common header formatting, checksum calculations, bounds checks) into private non-generic functions operating on byte slices &mut [u8], while preserving a clean generic public interface for static dispatch and type safety.
Answer
Implementation
use std::fmt;
#[derive(Debug, PartialEq, Eq)]
pub enum PacketError {
BufferTooSmall { required: usize, provided: usize },
InvalidPayload,
}
pub trait PacketPayload {
fn payload_type_id(&self) -> u8;
fn serialize_payload(&self, output: &mut [u8]) -> Result<usize, PacketError>;
}
pub struct EthernetHeader {
pub src_mac: [u8; 6],
pub dst_mac: [u8; 6],
pub ethertype: u16,
}
impl PacketPayload for EthernetHeader {
fn payload_type_id(&self) -> u8 {
0x01
}
fn serialize_payload(&self, output: &mut [u8]) -> Result<usize, PacketError> {
if output.len() < 14 {
return Err(PacketError::BufferTooSmall {
required: 14,
provided: output.len(),
});
}
output[0..6].copy_from_slice(&self.src_mac);
output[6..12].copy_from_slice(&self.dst_mac);
output[12..14].copy_from_slice(&self.ethertype.to_be_bytes());
Ok(14)
}
}
pub struct TelemetryPing {
pub timestamp_ms: u64,
pub node_id: u32,
}
impl PacketPayload for TelemetryPing {
fn payload_type_id(&self) -> u8 {
0x02
}
fn serialize_payload(&self, output: &mut [u8]) -> Result<usize, PacketError> {
if output.len() < 12 {
return Err(PacketError::BufferTooSmall {
required: 12,
provided: output.len(),
});
}
output[0..8].copy_from_slice(&self.timestamp_ms.to_be_bytes());
output[8..12].copy_from_slice(&self.node_id.to_be_bytes());
Ok(12)
}
}
pub struct PacketEncoder {
sequence_number: u32,
}
impl PacketEncoder {
pub fn new(sequence_number: u32) -> Self {
Self { sequence_number }
}
// NON-GENERIC HELPER 1: Common header construction compiled ONCE in binary.
fn format_common_header(
seq: u32,
payload_type: u8,
payload_len: usize,
buffer: &mut [u8],
) -> Result<usize, PacketError> {
let total_required = 8 + payload_len;
if buffer.len() < total_required {
return Err(PacketError::BufferTooSmall {
required: total_required,
provided: buffer.len(),
});
}
buffer[0..4].copy_from_slice(&seq.to_be_bytes());
buffer[4] = payload_type;
buffer[5] = 0x00; // Reserved flag byte
let len_bytes = (payload_len as u16).to_be_bytes();
buffer[6..8].copy_from_slice(&len_bytes);
Ok(8)
}
// NON-GENERIC HELPER 2: Checksum computation compiled ONCE in binary.
fn compute_checksum(data: &[u8]) -> u16 {
let mut checksum: u16 = 0;
for &byte in data {
checksum = checksum.wrapping_add(byte as u16);
}
checksum
}
// GENERIC ENTRY POINT: Thin monomorphized shim delegating heavy work to non-generic helpers.
pub fn encode_packet<P: PacketPayload>(
&mut self,
payload: &P,
out: &mut [u8],
) -> Result<usize, PacketError> {
if out.len() < 8 {
return Err(PacketError::BufferTooSmall {
required: 8,
provided: out.len(),
});
}
let payload_space = &mut out[8..];
let payload_written = payload.serialize_payload(payload_space)?;
let header_written = Self::format_common_header(
self.sequence_number,
payload.payload_type_id(),
payload_written,
out,
)?;
let total_data_len = header_written + payload_written;
let checksum = Self::compute_checksum(&out[..total_data_len]);
self.sequence_number = self.sequence_number.wrapping_add(1);
if out.len() >= total_data_len + 2 {
out[total_data_len..total_data_len + 2].copy_from_slice(&checksum.to_be_bytes());
Ok(total_data_len + 2)
} else {
Ok(total_data_len)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_monomorphized_ethernet_encoding() {
let mut encoder = PacketEncoder::new(100);
let eth = EthernetHeader {
src_mac: [0x00, 0x11, 0x22, 0x33, 0x44, 0x55],
dst_mac: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF],
ethertype: 0x0800,
};
let mut buf = [0u8; 32];
let res = encoder.encode_packet(ð, &mut buf);
assert!(res.is_ok());
let written = res.unwrap();
assert_eq!(written, 24); // 8 header + 14 payload + 2 checksum
assert_eq!(&buf[0..4], &[0, 0, 0, 100]); // Sequence number encoded as big-endian
assert_eq!(buf[4], 0x01); // Type ID for Ethernet
assert_ne!(encoder.sequence_number, 100); // Sequence number modified
}
#[test]
fn test_monomorphized_telemetry_ping_and_error_handling() {
let mut encoder = PacketEncoder::new(1);
let ping = TelemetryPing {
timestamp_ms: 1672531199000,
node_id: 42,
};
let mut small_buf = [0u8; 10];
let err_res = encoder.encode_packet(&ping, &mut small_buf);
assert!(err_res.is_err());
assert!(matches!(
err_res,
Err(PacketError::BufferTooSmall { required: _, provided: 10 })
));
let mut valid_buf = [0u8; 30];
let ok_res = encoder.encode_packet(&ping, &mut valid_buf);
assert!(ok_res.is_ok());
assert_eq!(ok_res.unwrap(), 22); // 8 header + 12 payload + 2 checksum
}
}
Technical Explanation
- Monomorphization Mechanics & Outlining:
- When
encode_packetis invoked forEthernetHeaderandTelemetryPing, the Rust compiler generates two specialized function entry points:encode_packet::<EthernetHeader>andencode_packet::<TelemetryPing>. - Without the outline pattern, the compiler would duplicate the full code body (header formatting, slice indexing, checksum loop, overflow wrapping) for every type
P. By delegating payload-agnostic work toformat_common_headerandcompute_checksum, the heavy machine instructions are emitted exactly once in the compiled binary. The generic entry points shrink to thin shims that perform static dispatch into the shared non-generic machine code.
- When
- Performance & Instruction Cache (I-Cache) Impact:
- Unconstrained monomorphization leads to "binary bloat," expanding executable sizes and causing CPU L1 instruction cache misses when processing many generic instantiations. Outlining maintains static dispatch performance while preserving L1 I-cache hit rates.
- Ownership, Lifetimes, and Slice Reborrowing:
encode_packetacceptsout: &mut [u8]. Slicing&mut out[8..]creates a sub-slice borrow. Rust's borrow checker enforces non-overlapping mutable access. Passingouttoformat_common_headerrequires distinct slice boundaries (&out[..total_data_len]), ensuring safety without runtime locks.
- Edge Cases:
- Overflow of sequence numbers is safely handled via
wrapping_add(1). - Buffer length validation prevents panic indexing errors by returning strongly-typed
PacketError::BufferTooSmall.
- Overflow of sequence numbers is safely handled via
Exercise 2: Type-Level Monomorphized Metric Aggregator vs Dynamic Dispatch
Scenario: Low-latency financial trading and storage engines rely on zero-allocation metrics collection. Using dynamic dispatch with trait objects (dyn MetricValue) incurs virtual function table (vtable) pointer lookups and indirect function calls (call [rax]), which disrupt CPU pipelining and prevent compiler inlining. Generic monomorphization replaces vtable lookups with direct, inlined static calls.
Implement a generic metric aggregator MetricAggregator<M: MetricValue> that monomorphizes specialized structures for concrete metrics (CounterMetric, GaugeMetric, HistogramMetric). Track sample counts, accumulated totals, std::any::TypeId, and type memory layout (size_of and align_of) without using dynamic trait objects (dyn). Write unit tests verifying type-level distinctness, static dispatch execution, and memory safety.
Answer
Implementation
use std::any::{type_name, TypeId};
use std::marker::PhantomData;
pub trait MetricValue: 'static {
fn name(&self) -> &'static str;
fn extract_value(&self) -> f64;
}
#[derive(Debug, Clone, PartialEq)]
pub struct CounterMetric {
pub value: u64,
}
impl MetricValue for CounterMetric {
fn name(&self) -> &'static str {
"counter"
}
fn extract_value(&self) -> f64 {
self.value as f64
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GaugeMetric {
pub level: f64,
}
impl MetricValue for GaugeMetric {
fn name(&self) -> &'static str {
"gauge"
}
fn extract_value(&self) -> f64 {
self.level
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct HistogramMetric {
pub sum: f64,
pub count: u64,
}
impl MetricValue for HistogramMetric {
fn name(&self) -> &'static str {
"histogram"
}
fn extract_value(&self) -> f64 {
if self.count == 0 {
0.0
} else {
self.sum / self.count as f64
}
}
}
// Specialized at compile time for metric type M
pub struct MetricAggregator<M: MetricValue> {
sample_count: u64,
accumulated_total: f64,
last_sample: f64,
_marker: PhantomData<M>,
}
impl<M: MetricValue> MetricAggregator<M> {
pub fn new() -> Self {
Self {
sample_count: 0,
accumulated_total: 0.0,
last_sample: 0.0,
_marker: PhantomData,
}
}
pub fn record(&mut self, metric: &M) {
let val = metric.extract_value();
self.sample_count += 1;
self.accumulated_total += val;
self.last_sample = val;
}
pub fn sample_count(&self) -> u64 {
self.sample_count
}
pub fn accumulated_total(&self) -> f64 {
self.accumulated_total
}
pub fn last_sample(&self) -> f64 {
self.last_sample
}
pub fn metric_type_id(&self) -> TypeId {
TypeId::of::<M>()
}
pub fn metric_type_name(&self) -> &'static str {
type_name::<M>()
}
pub fn memory_layout(&self) -> (usize, usize) {
(std::mem::size_of::<M>(), std::mem::align_of::<M>())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_monomorphized_counter_aggregation() {
let mut counter_agg = MetricAggregator::<CounterMetric>::new();
let c1 = CounterMetric { value: 10 };
let c2 = CounterMetric { value: 25 };
counter_agg.record(&c1);
counter_agg.record(&c2);
assert_eq!(counter_agg.sample_count(), 2);
assert_eq!(counter_agg.accumulated_total(), 35.0);
assert_eq!(counter_agg.last_sample(), 25.0);
assert!(counter_agg.metric_type_name().contains("CounterMetric"));
}
#[test]
fn test_distinct_monomorphized_type_identities() {
let counter_agg = MetricAggregator::<CounterMetric>::new();
let gauge_agg = MetricAggregator::<GaugeMetric>::new();
// Monomorphization creates two distinct compile-time types with distinct TypeIds
assert_ne!(counter_agg.metric_type_id(), gauge_agg.metric_type_id());
assert_ne!(counter_agg.metric_type_name(), gauge_agg.metric_type_name());
let (c_size, c_align) = counter_agg.memory_layout();
let (g_size, g_align) = gauge_agg.memory_layout();
assert_eq!(c_size, std::mem::size_of::<CounterMetric>());
assert_eq!(g_size, std::mem::size_of::<GaugeMetric>());
assert!(c_size > 0 && g_size > 0);
assert!(c_align > 0 && g_align > 0);
}
#[test]
fn test_histogram_monomorphization_and_pattern_matching() {
let mut hist_agg = MetricAggregator::<HistogramMetric>::new();
let h = HistogramMetric { sum: 100.0, count: 4 };
hist_agg.record(&h);
assert_eq!(hist_agg.last_sample(), 25.0);
let opt_agg: Option<MetricAggregator<HistogramMetric>> = Some(hist_agg);
assert!(matches!(opt_agg, Some(agg) if agg.sample_count() == 1));
}
}
Technical Explanation
- Static Monomorphization vs Dynamic Vtable Dispatch:
- In dynamic dispatch (
&dyn MetricValue), Rust generates a fat pointer containing a data pointer and a vtable pointer. Callingextract_value()resolves the address at runtime via vtable offset lookup (call [rax]). - With
MetricAggregator<M: MetricValue>, monomorphization synthesizes distinct concrete types (MetricAggregator<CounterMetric>,MetricAggregator<GaugeMetric>). Function calls tometric.extract_value()are bound at compile time, allowing LLVM to inlineextract_value()directly into therecord()machine code loop, achieving zero call overhead.
- In dynamic dispatch (
- Zero-Sized Type Marker (
PhantomData<M>):MetricAggregator<M>usesPhantomData<M>to signal type parameterMusage to the compiler without consuming heap or stack bytes. This enforces generic type safety at zero memory cost.
- Type Identification and Layout Invariants:
TypeId::of::<M>()provides unique, runtime-queryable type identification synthesized by the compiler for each monomorphized specialization.std::mem::size_of::<M>()andalign_of::<M>()return layout metrics determined during compile-time specialization.
- Edge Cases:
- Division by zero in
HistogramMetricwithcount == 0is safely checked, returning0.0instead ofNaNor triggering a runtime panic.
- Division by zero in
Exercise 3: Const Generic & Monomorphized Zero-Copy Hardware Buffer Serializer
Scenario: In embedded microcontrollers and real-time network hardware, heap memory allocation is strictly prohibited. Hardware Direct Memory Access (DMA) channels demand zero-copy serialization into stack-allocated fixed-capacity buffer registers. Rust generic code monomorphizes not only over type parameters T, but also over const generic parameters (const CAP: usize).
Design a zero-copy stack hardware serializer FixedBufferSerializer<T: ZeroCopyEncode, const CAP: usize> that serializes payloads into stack arrays [u8; CAP]. Demonstrate how combinations of type T and const buffer size CAP (e.g. (u32, 16), (u32, 64), (u64, 64)) produce distinct monomorphized structures and machine instructions. Implement strict capacity verification returning SerializationError::CapacityExceeded upon buffer exhaustions.
Answer
Implementation
use std::marker::PhantomData;
#[derive(Debug, PartialEq, Eq)]
pub enum SerializationError {
CapacityExceeded { required: usize, capacity: usize },
InvalidDataFormat,
}
pub trait ZeroCopyEncode {
fn encode_bytes(&self, target: &mut [u8]) -> Result<usize, SerializationError>;
}
impl ZeroCopyEncode for u32 {
fn encode_bytes(&self, target: &mut [u8]) -> Result<usize, SerializationError> {
if target.len() < 4 {
return Err(SerializationError::CapacityExceeded {
required: 4,
capacity: target.len(),
});
}
target[0..4].copy_from_slice(&self.to_be_bytes());
Ok(4)
}
}
impl ZeroCopyEncode for u64 {
fn encode_bytes(&self, target: &mut [u8]) -> Result<usize, SerializationError> {
if target.len() < 8 {
return Err(SerializationError::CapacityExceeded {
required: 8,
capacity: target.len(),
});
}
target[0..8].copy_from_slice(&self.to_be_bytes());
Ok(8)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceTelemetry {
pub device_id: u16,
pub status_code: u8,
pub uptime_seconds: u32,
}
impl ZeroCopyEncode for DeviceTelemetry {
fn encode_bytes(&self, target: &mut [u8]) -> Result<usize, SerializationError> {
if target.len() < 7 {
return Err(SerializationError::CapacityExceeded {
required: 7,
capacity: target.len(),
});
}
target[0..2].copy_from_slice(&self.device_id.to_be_bytes());
target[2] = self.status_code;
target[3..7].copy_from_slice(&self.uptime_seconds.to_be_bytes());
Ok(7)
}
}
// Monomorphized over BOTH type parameter T and const capacity CAP
pub struct FixedBufferSerializer<T: ZeroCopyEncode, const CAP: usize> {
buffer: [u8; CAP],
bytes_written: usize,
_marker: PhantomData<T>,
}
impl<T: ZeroCopyEncode, const CAP: usize> FixedBufferSerializer<T, CAP> {
pub fn new() -> Self {
Self {
buffer: [0u8; CAP],
bytes_written: 0,
_marker: PhantomData,
}
}
pub fn serialize(&mut self, item: &T) -> Result<&[u8], SerializationError> {
let remaining_space = &mut self.buffer[self.bytes_written..];
let n = item.encode_bytes(remaining_space)?;
let start = self.bytes_written;
self.bytes_written += n;
Ok(&self.buffer[start..self.bytes_written])
}
pub fn as_slice(&self) -> &[u8] {
&self.buffer[..self.bytes_written]
}
pub fn capacity(&self) -> usize {
CAP
}
pub fn remaining_capacity(&self) -> usize {
CAP - self.bytes_written
}
pub fn clear(&mut self) {
self.bytes_written = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_const_generic_monomorphization_u32_buffer() {
let mut serializer = FixedBufferSerializer::<u32, 16>::new();
assert_eq!(serializer.capacity(), 16);
assert_eq!(serializer.remaining_capacity(), 16);
let val: u32 = 0x12345678;
let slice_res = serializer.serialize(&val);
assert!(slice_res.is_ok());
assert_eq!(slice_res.unwrap(), &[0x12, 0x34, 0x56, 0x78]);
assert_eq!(serializer.remaining_capacity(), 12);
}
#[test]
fn test_const_generic_type_and_capacity_distinctness() {
let ser_64 = FixedBufferSerializer::<u32, 64>::new();
let ser_128 = FixedBufferSerializer::<u32, 128>::new();
let ser_u64_64 = FixedBufferSerializer::<u64, 64>::new();
// Distinct const generic capacities yield distinct type memory layouts
assert_ne!(ser_64.capacity(), ser_128.capacity());
assert_eq!(ser_64.capacity(), ser_u64_64.capacity());
assert_eq!(
std::mem::size_of::<FixedBufferSerializer<u32, 64>>(),
std::mem::size_of::<FixedBufferSerializer<u64, 64>>()
);
assert_ne!(
std::mem::size_of::<FixedBufferSerializer<u32, 64>>(),
std::mem::size_of::<FixedBufferSerializer<u32, 128>>()
);
}
#[test]
fn test_capacity_exceeded_error_matching() {
let mut tiny_serializer = FixedBufferSerializer::<DeviceTelemetry, 5>::new();
let telemetry = DeviceTelemetry {
device_id: 1,
status_code: 200,
uptime_seconds: 3600,
};
let res = tiny_serializer.serialize(&telemetry);
assert!(res.is_err());
assert!(matches!(
res,
Err(SerializationError::CapacityExceeded { required: 7, capacity: 5 })
));
}
}
Technical Explanation
- Multi-Dimensional Monomorphization (Types & Const Generics):
- The compiler monomorphizes code across the Cartesian product of type parameters
Tand const parametersCAP. FixedBufferSerializer<u32, 64>andFixedBufferSerializer<u32, 128>are distinct types in the compiler's type system with different memory representations ([u8; 64]vs[u8; 128]). Stack frame sizing, struct offset calculations, and method machine code are emitted independently for each concrete tuple(T, CAP).
- The compiler monomorphizes code across the Cartesian product of type parameters
- Zero Heap Allocations & Direct Stack Layout:
- Fixed byte array
[u8; CAP]is allocated directly within the struct instance on the stack. No calls to global allocators (malloc/jemalloc) occur, ensuring deterministic execution critical for embedded RTOS and real-time systems.
- Fixed byte array
- Ownership and Slice Lifetimes:
- Method
serialize(&mut self, item: &T)returns a sliceResult<&[u8], SerializationError>tied to the lifetime of&mut self. This guarantees the caller cannot mutate the buffer while holding a reference to the serialized slice.
- Method
- Edge Cases:
- Exhausting array capacity returns
SerializationError::CapacityExceededwithout causing slice out-of-bounds panics.clear()resetsbytes_writtento zero for buffer reuse without reallocating stack memory.
- Exhausting array capacity returns
6. Related Terms
- Generics (
<T>) — The language feature that triggers Monomorphization. - Trait Objects (
dyn Trait) — The exact opposite of Monomorphization. Trait objects use dynamic dispatch at runtime. They save file size (no copy-pasting code) but cost runtime performance. ZSTs(Zero-Sized Types) — Related concept:ZSTs(Zero-Sized Types).- Zero-Cost Abstractions — Related concept: Zero-Cost Abstractions.
7. Key Takeaways
- Monomorphization is the compiler turning generic code into specific, hard-coded code at compile-time.
- "Mono" (one) + "morph" (form) = turning a generic into one specific form.
- It is a Zero-Cost Abstraction. Using a generic function in Rust is exactly as fast as manually writing a hard-coded function for that specific type.
- The only trade-offs are slightly longer compile times and larger executable file sizes (binary bloat).