How does strum::EnumCount derive compile-time variant counting for use with array sizing?

EnumCount derives a COUNT constant equal to the number of variants in an enum, computed at compile time through procedural macro expansion, enabling fixed-size array declarations sized precisely to the enum's variant count without runtime calculation or heap allocation. The COUNT constant is available as an associated constant on the enum type, allowing arrays to be sized with [T; EnumType::COUNT] where the size matches the number of variants exactly.

The EnumCount Trait and Derive

use strum::EnumCount;
 
// The EnumCount trait provides a compile-time constant for variant count
pub trait EnumCount {
    const COUNT: usize;
}
 
// Deriving EnumCount generates the COUNT constant
#[derive(Debug, Clone, Copy, EnumCount)]
enum Direction {
    North,
    South,
    East,
    West,
}
 
fn basics() {
    // COUNT is a compile-time constant
    const NUM_DIRECTIONS: usize = Direction::COUNT;
    assert_eq!(NUM_DIRECTIONS, 4);
    
    // Can be used in const contexts
    let array: [i32; Direction::COUNT] = [0, 1, 2, 3];
    assert_eq!(array.len(), 4);
}

The COUNT constant is computed during compilation, enabling use in type parameters and const generics.

How the Macro Works

// When you write:
#[derive(Debug, Clone, Copy, EnumCount)]
enum Direction {
    North,
    South,
    East,
    West,
}
 
// The procedural macro generates:
impl EnumCount for Direction {
    const COUNT: usize = 4;  // Computed from variant count
}
 
// The macro:
// 1. Parses the enum definition
// 2. Counts the number of variants
// 3. Generates the impl with the literal count value
// 4. This happens at compile time via procedural macro
 
// For an enum with 10 variants, it generates COUNT = 10
// The value is literally written into the generated code

The derive macro counts variants and emits a literal constant in the generated impl.

Array Sizing with COUNT

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Status {
    Pending,
    Active,
    Completed,
    Failed,
}
 
fn array_sizing() {
    // Create an array sized to the number of variants
    let mut counters: [u32; Status::COUNT] = [0; Status::COUNT];
    
    // Now we can index by discriminant (with IntoEnumIterator)
    // Or use the array for variant-specific data
    counters[Status::Pending as usize] += 1;
    counters[Status::Active as usize] += 1;
    
    assert_eq!(counters.len(), 4);
    
    // The array size is known at compile time
    // No Vec allocation needed
    // No runtime counting required
}

Arrays sized with COUNT have their size verified at compile time.

Const Context Usage

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Color {
    Red,
    Green,
    Blue,
}
 
// COUNT can be used in const contexts
const NUM_COLORS: usize = Color::COUNT;
 
// For array type definitions
type ColorValues = [f32; Color::COUNT];
 
// For const generics (Rust 1.51+)
struct LookupTable<T, const N: usize> {
    values: [T; N],
}
 
impl Color {
    fn lookup_table() -> LookupTable<f32, { Self::COUNT }> {
        LookupTable { values: [0.0; Self::COUNT] }
    }
}
 
// In const functions
const fn get_color_count() -> usize {
    Color::COUNT
}

The constant nature of COUNT enables use in const contexts where runtime values are forbidden.

Combining with IntoEnumIterator

use strum::{EnumCount, IntoEnumIterator};
 
#[derive(Debug, Clone, Copy, PartialEq, EnumCount, IntoEnumIterator)]
enum Token {
    Identifier,
    Number,
    String,
    Operator,
    Keyword,
}
 
fn with_iterator() {
    // Use COUNT to size an array
    let mut token_counts: [usize; Token::COUNT] = [0; Token::COUNT];
    
    // Iterate over all variants and update corresponding array entry
    for token in Token::iter() {
        let index = token as usize;
        token_counts[index] += 1;  // Or use token-specific logic
    }
    
    // This combination is powerful:
    // - COUNT defines array size
    // - IntoEnumIterator iterates all variants
    // - Discriminant provides array index
    // - Zero-cost abstraction for variant data
}

EnumCount pairs naturally with IntoEnumIterator for complete variant handling.

Variant Data Arrays

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum MouseButton {
    Left,
    Middle,
    Right,
    Back,
    Forward,
}
 
fn variant_data_arrays() {
    // Store metadata per variant
    let button_names: [&str; MouseButton::COUNT] = [
        "Left",
        "Middle",
        "Right",
        "Back",
        "Forward",
    ];
    
    // Store configuration per variant
    let button_icons: [char; MouseButton::COUNT] = [
        'šŸ–±',  // Left
        'āš™',   // Middle
        'šŸ–±',  // Right
        '⬅',   // Back
        'āž”',   // Forward
    ];
    
    // Access by index
    fn get_button_name(button: MouseButton) -> &'static str {
        button_names[button as usize]
    }
}

Arrays indexed by variant discriminant provide efficient variant-to-data mapping.

Compile-Time Validation

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Priority {
    Low,
    Medium,
    High,
    Critical,
}
 
fn compile_time_validation() {
    // The compiler verifies array size matches COUNT
    let valid: [u8; Priority::COUNT] = [1, 2, 3, 4];  // Compiles
    
    // This would fail to compile:
    // let invalid: [u8; Priority::COUNT] = [1, 2, 3];  // Error: expected 4 elements
    
    // This safety extends to all uses:
    // - If you add a variant, COUNT increases
    // - Arrays sized with COUNT will fail to compile
    // - Compiler forces you to handle new variants
}

Using COUNT for array sizing catches mismatched sizes at compile time.

Comparison with Runtime Counting

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy)]
enum Status {
    Pending,
    Active,
    Done,
}
 
// Runtime counting methods:
 
// Method 1: Hardcoded constant
// Problem: Can get out of sync
fn runtime_hardcoded() {
    const STATUS_COUNT: usize = 3;  // Must manually update when adding variants
    let arr: [i32; STATUS_COUNT] = [0; 3];
}
 
// Method 2: Enum iterator and count()
// Problem: Runtime computation, can't use in const contexts
fn runtime_iterator() {
    use strum::IntoEnumIterator;
    let count = Status::iter().count();  // Runtime count
    // Can't use: let arr: [i32; count] = ...;  // Not const
    let vec: Vec<i32> = vec![0; count];  // Requires heap allocation
}
 
// Method 3: EnumCount derive
// Benefits: Compile-time, no heap, const-compatible
fn compile_time_count() {
    // COUNT is computed at compile time
    let arr: [i32; Status::COUNT] = [0; 3];  // Valid
    
    // Works in const contexts
    const STATUSES: usize = Status::COUNT;
}

EnumCount provides compile-time count without manual maintenance or runtime cost.

Generic Contexts

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Opcode {
    Add,
    Sub,
    Mul,
    Div,
}
 
// Generic function using COUNT
fn create_lookup_table<E: EnumCount, T: Default + Copy>() -> [T; E::COUNT] {
    [T::default(); E::COUNT]  // Uses the associated const generically
}
 
// Generic struct using COUNT
struct EnumMap<E: EnumCount, V> {
    values: [V; E::COUNT],
}
 
impl<E: EnumCount + Into<usize>, V: Default + Copy> EnumMap<E, V> {
    fn new() -> Self {
        Self {
            values: [V::default(); E::COUNT],
        }
    }
    
    fn get(&self, key: E) -> &V {
        &self.values[key as usize]
    }
    
    fn set(&mut self, key: E, value: V) {
        self.values[key as usize] = value;
    }
}

COUNT can be used generically when the type implements EnumCount.

Handling Enum Changes

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Event {
    Click,
    KeyPress,
    MouseMove,
}
 
fn handling_changes() {
    // Array sized with COUNT
    let handlers: [fn(); Event::COUNT] = [
        handle_click,
        handle_keypress,
        handle_mouse_move,
    ];
    
    // When you add a new variant:
    // #[derive(Debug, Clone, Copy, EnumCount)]
    // enum Event {
    //     Click,
    //     KeyPress,
    //     MouseMove,
    //     Scroll,  // New variant
    // }
    
    // The array will fail to compile:
    // error: expected an array with a fixed size of 4 elements,
    //        found one with 3 elements
    
    // This forces you to add the handler:
    // let handlers: [fn(); Event::COUNT] = [
    //     handle_click,
    //     handle_keypress,
    //     handle_mouse_move,
    //     handle_scroll,  // Must add
    // ];
}

COUNT ensures array sizes stay synchronized with variant counts.

Enums with Associated Data

use strum::EnumCount;
 
// EnumCount works with enums that have associated data
#[derive(Debug, Clone, EnumCount)]
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
 
fn with_associated_data() {
    // COUNT still counts VARIANTS, not struct fields
    assert_eq!(Message::COUNT, 4);
    
    // Array sized to number of variants
    let message_handlers: [&str; Message::COUNT] = [
        "quit",
        "move",
        "write",
        "change_color",
    ];
    
    // The COUNT is based on variant count, ignoring payload
}

COUNT counts variants regardless of their associated data.

Nested Enums

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Outer {
    A,
    B,
}
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Inner {
    X,
    Y,
    Z,
}
 
fn nested_enums() {
    // Each enum has its own COUNT
    assert_eq!(Outer::COUNT, 2);
    assert_eq!(Inner::COUNT, 3);
    
    // Can create arrays for combinations
    let combinations: [i32; { Outer::COUNT * Inner::COUNT }] = [0; 6];
    
    // Or nested arrays
    let nested: [[i32; Inner::COUNT]; Outer::COUNT] = [
        [1, 2, 3],
        [4, 5, 6],
    ];
}

Each enum's COUNT is independent, allowing composition in complex array types.

Performance Characteristics

use strum::EnumCount;
 
fn performance() {
    // COUNT is a compile-time constant
    // Zero runtime overhead
    
    // These compile to the same thing:
    let arr1: [i32; Status::COUNT] = [0; Status::COUNT];
    let arr2: [i32; 4] = [0; 4];  // Status has 4 variants
    
    // The compiler sees COUNT as a literal constant
    // No runtime computation whatsoever
    
    // In optimized builds, even array indexing
    // with COUNT-based sizes may be fully inlined
    
    // Compare to runtime counting:
    // let count = Status::iter().count();  // Runs iterator at runtime
    // let vec: Vec<i32> = vec![0; count];  // Heap allocation
    
    // EnumCount approach:
    // - No runtime overhead
    // - No heap allocation
    // - Compiler-verified sizes
}

COUNT has zero runtime overhead—it's literally a constant in the generated code.

When to Use EnumCount

use strum::EnumCount;
 
fn when_to_use() {
    // Use EnumCount when:
    
    // 1. You need a fixed-size array indexed by enum variants
    let counters: [u32; MyEnum::COUNT] = [0; MyEnum::COUNT];
    
    // 2. You need the count in const contexts
    const MAX_VARIANTS: usize = MyEnum::COUNT;
    
    // 3. You want compile-time verification of array sizes
    // (compiler will catch if array and COUNT don't match)
    
    // 4. You're building lookup tables or caches
    let lut: [f32; MyEnum::COUNT] = compute_lut();
    
    // 5. You need zero-allocation data structures
    // Arrays on stack instead of Vec on heap
    
    // Don't use when:
    // - You only need to iterate variants (use IntoEnumIterator)
    // - You need variant names (use EnumVariantNames)
    // - You need to convert strings to variants (use FromStr)
    // - The count varies at runtime (impossible for enums)
}

EnumCount is ideal for fixed-size arrays indexed by enum variants.

Complete Example: State Machine

use strum::{EnumCount, IntoEnumIterator};
 
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumCount, IntoEnumIterator)]
enum State {
    Idle,
    Connecting,
    Connected,
    Disconnecting,
    Error,
}
 
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumCount, IntoEnumIterator)]
enum Event {
    Connect,
    Connected,
    Disconnect,
    Disconnected,
    Error,
    Retry,
}
 
// Transition table: [State::COUNT][Event::COUNT] -> Option<State>
type TransitionTable = [[Option<State>; Event::COUNT]; State::COUNT];
 
fn state_machine() {
    let transitions: TransitionTable = [
        // Idle state
        [Some(State::Connecting), None, None, None, None, None],
        // Connecting state
        [None, Some(State::Connected), Some(State::Disconnecting), None, Some(State::Error), Some(State::Connecting)],
        // Connected state
        [None, None, Some(State::Disconnecting), None, Some(State::Error), None],
        // Disconnecting state
        [None, None, None, Some(State::Idle), Some(State::Error), None],
        // Error state
        [Some(State::Connecting), None, None, None, None, Some(State::Connecting)],
    ];
    
    // Lookup transition: O(1) array access
    fn next_state(current: State, event: Event, table: &TransitionTable) -> Option<State> {
        table[current as usize][event as usize]
    }
    
    // State is indexed by discriminant
    // Event is indexed by discriminant
    // Array size is compile-time verified
    
    assert_eq!(next_state(State::Idle, Event::Connect, &transitions), Some(State::Connecting));
    assert_eq!(next_state(State::Connected, Event::Disconnect, &transitions), Some(State::Disconnecting));
}

State machines benefit from COUNT for transition table sizing.

Summary Table

fn summary() {
    // | Feature | EnumCount | Runtime count() |
    // |---------|-----------|-----------------|
    // | When computed | Compile time | Runtime |
    // | Const context | Yes | No |
    // | Array sizing | Yes | No |
    // | Heap allocation | No | Yes (Vec) |
    // | Runtime cost | Zero | Iterator overhead |
    // | Maintenance | Automatic | Manual |
    
    // | Use Case | Recommended Approach |
    // |----------|----------------------|
    // | Fixed-size arrays | EnumCount::COUNT |
    // | Const generics | EnumCount::COUNT |
    // | Stack allocation | EnumCount::COUNT |
    // | Iterating variants | IntoEnumIterator |
    // | Getting variant names | EnumVariantNames |
    // | String conversion | FromStr/IntoStaticStr |
}

Synthesis

Quick reference:

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Status {
    Pending,
    Active,
    Completed,
    Failed,
}
 
fn quick_reference() {
    // COUNT is a compile-time constant
    assert_eq!(Status::COUNT, 4);
    
    // Use for array sizing
    let counters: [u32; Status::COUNT] = [0; Status::COUNT];
    
    // Use in const contexts
    const NUM_STATUSES: usize = Status::COUNT;
    
    // Use in const generics
    struct Lookup<T, const N: usize> {
        values: [T; N],
    }
    let lookup: Lookup<f32, { Status::COUNT }> = Lookup { values: [0.0; Status::COUNT] };
}

Key insight: strum::EnumCount provides a zero-cost compile-time mechanism for obtaining the number of enum variants. The procedural macro counts variants during compilation and generates an associated constant COUNT with the literal value, enabling use in array type parameters, const generics, and other contexts requiring compile-time values. This eliminates the maintenance burden of manually tracking variant counts and provides compile-time verification that array sizes match variant counts. When combined with IntoEnumIterator and discriminants, you can build efficient O(1) lookup tables sized precisely to the enum's structure—all on the stack without heap allocation. The primary use case is fixed-size arrays indexed by enum variants, where COUNT ensures the array size remains synchronized with the number of variants as the enum evolves.