Constructors and Initialization
Rust has no constructor keyword. What it has is a stricter rule: every field must be initialized before the struct can be used. The new() function is simply the conventional way to centralize that initialization.
Uninitialized fields are a compile error
#![allow(unused)] fn main() { // ERROR: missing field `sp` in initializer of `Cpu` let cpu = Cpu { v: [0; 16], i: 0, pc: 0x200, stack: [0; 16], // sp forgotten — does not compile }; }
Unlike C, there is no garbage memory waiting to cause a crash at runtime. The compiler ensures every value is set before it is read.
Why new() exists
Without it, every caller has to know all the initial values:
#![allow(unused)] fn main() { // without new() — caller must know everything let cpu = Cpu { v: [0;16], i: 0, pc: 0x200, sp: 0, stack: [0;16], memory: ..., display: ... }; // with new() — clean and encapsulated let cpu = Cpu::new(); }
new() centralizes the default values in one place. If initialization logic changes — say, fonts need to be loaded into memory at startup — you change new() and nothing else.
The Default trait
Rust provides a Default trait for types with a natural "zero" value. It can be derived:
#![allow(unused)] fn main() { #[derive(Default)] pub struct Cpu { ... } let cpu = Cpu::default(); }
The limitation: #[derive(Default)] sets every field to its type's zero (0 for integers, false for booleans). For Cpu, this is wrong — pc must start at 0x200, not 0. So we write new() manually.
You can bridge both approaches by delegating:
#![allow(unused)] fn main() { impl Default for Cpu { fn default() -> Self { Self::new() } } }
This makes Cpu work with parts of the ecosystem that expect Default, while keeping the initialization logic in new().
In the emulator:
Cpu::new()initializes PC to0x200, zeroes all registers, and delegates toMemory::new()andDisplay::new().Memory::new()also loads the 16 font sprites into RAM at address0x000— somethingDefaultcould not express. See Font Sprites for why fonts must be in memory from the start.