When should you use strum::EnumCount to get the number of enum variants at compile time?

strum::EnumCount provides a COUNT constant containing the number of enum variants, computed at compile time through procedural macros. Use it when you need the variant count as a constant for array sizing, validation boundaries, or compile-time assertions—situations where runtime iteration over variants would be inefficient or impossible. The key advantage is having VARIANT_COUNT available as a const value that the compiler can use for optimizations, unlike iter().count() which computes at runtime. Avoid EnumCount when the count is only needed for informational display or when the enum has many variants with significant compile-time cost from the macro expansion.

The EnumCount Derive

use strum::EnumCount;
 
#[derive(Debug, Clone, EnumCount)]
enum Color {
    Red,
    Green,
    Blue,
}
 
fn main() {
    println!("Number of colors: {}", Color::COUNT);
    // Number of colors: 3
}

The #[derive(EnumCount)] macro generates a COUNT constant.

Using COUNT for Array Sizing

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Direction {
    North,
    East,
    South,
    West,
}
 
// Use COUNT to size arrays appropriately
fn main() {
    // Array sized exactly for one value per variant
    let mut direction_counts: [u32; Direction::COUNT] = [0; Direction::COUNT];
    
    let movements = [Direction::North, Direction::South, Direction::East, Direction::North];
    
    for dir in movements {
        direction_counts[dir as usize] += 1;
    }
    
    println!("North: {}, East: {}, South: {}, West: {}",
        direction_counts[0], direction_counts[1],
        direction_counts[2], direction_counts[3]);
}

COUNT enables compile-time array sizing with exact variant capacity.

Compile-Time Assertions

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Status {
    Pending,
    Processing,
    Completed,
    Failed,
}
 
// Assert at compile time that we have expected variants
const _: () = assert!(Status::COUNT == 4, "Status enum should have exactly 4 variants");
 
// This pattern catches accidental variant additions/removals
fn main() {
    let status_names: [&str; Status::COUNT] = ["Pending", "Processing", "Completed", "Failed"];
    println!("All {} statuses defined", Status::COUNT);
}

Compile-time assertions catch enum changes during build.

Alternative: Runtime Iteration

use strum::{EnumCount, IntoEnumIterator};
 
#[derive(Debug, Clone, Copy, EnumCount, IntoEnumIterator)]
enum Priority {
    Low,
    Medium,
    High,
}
 
fn main() {
    // Using EnumCount - compile time constant
    println!("EnumCount: {}", Priority::COUNT);
    
    // Using iterator - runtime computation
    let runtime_count = Priority::iter().count();
    println!("Runtime count: {}", runtime_count);
    
    // COUNT is evaluated at compile time
    // iter().count() is evaluated at runtime
}

EnumCount gives a constant; iter().count() computes each time.

Static Dispatch with COUNT

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Resource {
    Wood,
    Stone,
    Gold,
}
 
// Compile-time sized array enables stack allocation
fn main() {
    let resource_amounts: [u64; Resource::COUNT] = [100, 50, 25];
    
    // Stack-allocated, no heap allocation needed
    let mut total: u64 = resource_amounts.iter().sum();
    println!("Total resources: {}", total);
}

Exact sizing enables stack-allocated arrays without vectors.

Comparison with std::mem::variant_count

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Level {
    Beginner,
    Intermediate,
    Advanced,
}
 
fn main() {
    // strum::EnumCount
    const COUNT1: usize = Level::COUNT;
    
    // std::mem::variant_count (nightly feature)
    // const COUNT2: usize = std::mem::variant_count::<Level>();
    
    println!("Level count: {}", COUNT1);
}

std::mem::variant_count is similar but requires nightly; EnumCount works on stable.

When to Use EnumCount

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum ErrorCode {
    NetworkTimeout,
    InvalidInput,
    Unauthorized,
    NotFound,
    InternalError,
}
 
// Use case 1: Fixed-size arrays for per-variant data
fn get_error_messages() -> [&'static str; ErrorCode::COUNT] {
    [
        "Network timeout occurred",
        "Invalid input provided",
        "Unauthorized access",
        "Resource not found",
        "Internal server error",
    ]
}
 
// Use case 2: Validation boundaries
fn is_valid_error_code(code: u8) -> bool {
    (code as usize) < ErrorCode::COUNT
}
 
// Use case 3: Compile-time size checks
fn main() {
    const MESSAGES: [&str; ErrorCode::COUNT] = [
        "Network timeout occurred",
        "Invalid input provided",
        "Unauthorized access",
        "Resource not found",
        "Internal server error",
    ];
    
    // Array length matches COUNT exactly
    for (i, msg) in MESSAGES.iter().enumerate() {
        println!("Error {}: {}", i, msg);
    }
}

Use EnumCount for compile-time sized structures and boundary checks.

When NOT to Use EnumCount

use strum::{EnumCount, IntoEnumIterator};
 
#[derive(Debug, Clone, EnumCount, IntoEnumIterator)]
enum HttpStatus {
    Ok,
    NotFound,
    ServerError,
}
 
fn main() {
    // DON'T use EnumCount just for display
    println!("We have {} statuses", HttpStatus::COUNT);
    // Just use the literal or a constant instead
    
    // DON'T use EnumCount when you need to iterate anyway
    for status in HttpStatus::iter() {
        println!("{:?}", status);
    }
    // If iterating, iter().count() is free since you're already iterating
    
    // DON'T use EnumCount for dynamic enums
    // EnumCount is compile-time; can't handle dynamic variant counts
}

Avoid EnumCount for simple display or when already iterating.

Per-Variant Storage Pattern

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum StatType {
    Strength,
    Dexterity,
    Constitution,
    Intelligence,
    Wisdom,
    Charisma,
}
 
struct Character {
    // Fixed-size array sized by variant count
    stats: [u8; StatType::COUNT],
}
 
impl Character {
    fn new() -> Self {
        Self {
            stats: [10; StatType::COUNT],
        }
    }
    
    fn get_stat(&self, stat: StatType) -> u8 {
        self.stats[stat as usize]
    }
    
    fn set_stat(&mut self, stat: StatType, value: u8) {
        self.stats[stat as usize] = value;
    }
}
 
fn main() {
    let mut char = Character::new();
    char.set_stat(StatType::Strength, 18);
    char.set_stat(StatType::Intelligence, 14);
    
    println!("STR: {}, INT: {}", 
        char.get_stat(StatType::Strength),
        char.get_stat(StatType::Intelligence));
}

COUNT enables direct array indexing by variant.

Compile-Time Bounds Checking

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Tile {
    Grass,
    Water,
    Sand,
    Rock,
}
 
fn main() {
    // Array exactly sized for tile types
    let tile_colors: [u32; Tile::COUNT] = [
        0x00FF00, // Grass - green
        0x0000FF, // Water - blue
        0xFFFF00, // Sand - yellow
        0x808080, // Rock - gray
    ];
    
    // This would fail to compile if Tile::COUNT doesn't match array length
    let _colors: [u32; Tile::COUNT] = tile_colors;
    
    // Compile-time guarantee that indexing won't panic
    fn get_color(tile: Tile) -> u32 {
        let colors: [u32; Tile::COUNT] = [
            0x00FF00, 0x0000FF, 0xFFFF00, 0x808080
        ];
        colors[tile as usize] // Safe: index < COUNT guaranteed
    }
}

The array length must match COUNT, catching mismatches at compile time.

Integration with IntoEnumIterator

use strum::{EnumCount, IntoEnumIterator};
 
#[derive(Debug, Clone, Copy, EnumCount, IntoEnumIterator)]
enum Flag {
    A,
    B,
    C,
}
 
fn main() {
    // COUNT for sizing
    let mut flag_values: [bool; Flag::COUNT] = [false; Flag::COUNT];
    
    // iter() for iteration
    for flag in Flag::iter() {
        flag_values[flag as usize] = true;
    }
    
    // Both together: exact sizing + clean iteration
    println!("Flags set: {:?}", flag_values);
    
    // COUNT matches iterator count
    assert_eq!(Flag::COUNT, Flag::iter().count());
}

EnumCount and IntoEnumIterator work well together.

Performance Considerations

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Small {
    A,
    B,
    C,
}
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Large {
    V0, V1, V2, V3, V4, V5, V6, V7, V8, V9,
    V10, V11, V12, V13, V14, V15, V16, V17, V18, V19,
    // ... many more variants
}
 
fn main() {
    // COUNT is compile-time constant - no runtime overhead
    let small_size: usize = Small::COUNT;
    let large_size: usize = Large::COUNT;
    
    // Both are just constant values in the binary
    // No iteration or computation at runtime
    
    // Macro expansion happens at compile time
    // For very large enums, this adds to compile time, not runtime
}

COUNT is a compile-time constant with zero runtime cost.

Generic Code with COUNT

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Input {
    Keyboard,
    Mouse,
    Controller,
}
 
fn main() {
    // COUNT can be used in const generics (with limitations)
    const INPUT_COUNT: usize = Input::COUNT;
    
    // Create fixed-size array for input states
    let input_states: [bool; INPUT_COUNT] = [false; INPUT_COUNT];
    
    // COUNT works in const contexts
    const MAX_INPUTS: usize = Input::COUNT;
    println!("Max inputs: {}", MAX_INPUTS);
}

COUNT is usable in const contexts for fixed sizing.

Validation with COUNT

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum Permission {
    Read,
    Write,
    Execute,
}
 
struct PermissionSet {
    // One bit per permission
    bits: u32,
}
 
impl PermissionSet {
    fn has(&self, perm: Permission) -> bool {
        self.bits & (1 << (perm as usize)) != 0
    }
    
    fn set(&mut self, perm: Permission) {
        self.bits |= 1 << (perm as usize);
    }
    
    fn all() -> Self {
        // All bits up to COUNT set
        Self {
            bits: (1u32 << Permission::COUNT) - 1,
        }
    }
}
 
fn main() {
    let all_perms = PermissionSet::all();
    println!("Has Read: {}", all_perms.has(Permission::Read));
}

COUNT enables bitwise operations bounded by variant count.

Practical Example: Game State

use strum::EnumCount;
 
#[derive(Debug, Clone, Copy, EnumCount)]
enum PlayerSlot {
    Player1,
    Player2,
    Player3,
    Player4,
}
 
struct GameState {
    // Fixed array for player scores
    scores: [u32; PlayerSlot::COUNT],
    // Fixed array for player names
    names: [Option<String>; PlayerSlot::COUNT],
}
 
impl GameState {
    fn new() -> Self {
        Self {
            scores: [0; PlayerSlot::COUNT],
            names: [None; PlayerSlot::COUNT],
        }
    }
    
    fn add_score(&mut self, slot: PlayerSlot, points: u32) {
        self.scores[slot as usize] += points;
    }
}
 
fn main() {
    let mut game = GameState::new();
    game.add_score(PlayerSlot::Player1, 100);
    game.add_score(PlayerSlot::Player2, 150);
    
    println!("Player 1 score: {}", game.scores[PlayerSlot::Player1 as usize]);
}

Game state often uses fixed-size arrays indexed by enum.

Summary Table

Use Case Appropriate? Reason
Array sizing Yes Compile-time constant
Validation bounds Yes Zero runtime cost
Compile-time assertions Yes Catches enum changes
Bit manipulation Yes Known bit count
Display/informational No Use literal or doc
Already iterating No Use iter().count()
Dynamic counting No Must be compile-time

Synthesis

strum::EnumCount provides compile-time variant counts for sizing and validation:

When to use it:

  • Sizing arrays that store per-variant data—[T; Enum::COUNT] guarantees exact sizing
  • Boundary validation—checking integer values against COUNT without runtime iteration
  • Compile-time assertions—catching accidental enum modifications during build
  • Bit manipulation—calculating bit masks bounded by variant count
  • Generic code needing fixed-size containers indexed by enum

When to avoid it:

  • Purely informational display—use a documented constant or just show the variants
  • When already iterating—iter().count() adds no overhead if you're already calling iter()
  • Simple comparisons—status == Status::COUNT is clearer written directly

Key insight: EnumCount trades compile-time macro expansion for zero-cost runtime constant access. The COUNT constant enables patterns that would otherwise require vectors or runtime iteration. It's most valuable when the variant count directly influences data structure sizing or validation logic, turning what would be a runtime check into a compile-time guarantee.

The derive macro is simple but powerful: it gives you VARIANT_COUNT as a const value, enabling all the patterns that require compile-time sizing. Use it when array sizing or boundary constants matter; skip it when a simple numeric literal would suffice.