· 6 min read ·

cfg_select! Is in Rust 1.95, and Its History Is Half the Story

Source: rust

Rust 1.95.0 landed on April 16, 2026, bringing two features that have been in various states of discussion and implementation for years: the cfg_select! macro and if-let guards in match expressions. Neither is a headline breakthrough. Both address ergonomics gaps that have existed since Rust’s early years and will quietly improve a lot of real code.

The cfg-if problem

If you have written cross-platform Rust for any length of time, you have reached for conditional compilation. The #[cfg(...)] attribute gets you far for simple cases:

#[cfg(unix)]
fn platform_init() { /* unix */ }

#[cfg(not(unix))]
fn platform_init() { /* everything else */ }

This falls apart the moment you have three or more branches, or need to wrap larger items like impl blocks, use declarations, or module-level structs across platforms. You start writing the same configuration conditions multiple times and manually inverting them. The not(unix) form is already a trap: what you mean is “fallback,” but not(unix) will catch platforms you never intended if the target list grows.

Alex Crichton wrote the cfg-if crate to address this. It defines a cfg_if! macro that gives you a proper if/else-if/else structure for configurations:

cfg_if::cfg_if! {
    if #[cfg(unix)] {
        fn platform_init() { /* unix */ }
    } else if #[cfg(target_os = "windows")] {
        fn platform_init() { /* windows */ }
    } else {
        fn platform_init() { /* fallback */ }
    }
}

cfg-if is sitting at well over a billion downloads on crates.io. It appears as a dependency in the Rust standard library itself, in virtually every systems-level crate, and in most cross-platform projects of any size. That scale of adoption is its own argument for standardization.

The syntax has always been a bit awkward, though. if #[cfg(condition)] merges two distinct Rust syntaxes, the if keyword and the attribute notation, into something you have to parse as two layers simultaneously. It works, but it reads like a macro workaround, which is exactly what it is.

What cfg_select! looks like

The new cfg_select! macro, stabilized in 1.95, uses match arm syntax instead:

cfg_select! {
    unix => {
        fn platform_init() { /* unix */ }
    }
    target_os = "windows" => {
        fn platform_init() { /* windows */ }
    }
    _ => {
        fn platform_init() { /* fallback */ }
    }
}

The _ arm is the explicit fallback, the same role it plays in a match expression. The semantics are straightforward: Rust evaluates each arm’s configuration predicate in order and expands the first one that evaluates to true at compile time. Only one arm ever reaches the output binary.

The macro also works in expression position, which is where it gets genuinely convenient:

let arch_name = cfg_select! {
    target_arch = "x86_64" => "x86_64",
    target_arch = "aarch64" => "aarch64",
    target_arch = "wasm32" => "wasm32",
    _ => "unknown",
};

That is cleaner than the cfg-if equivalent and cleaner than the attribute approach for anything beyond a single binary choice. More importantly, it is a syntax that Rust developers already know how to read.

The crate-to-stdlib arc

Beyond the syntax improvement, the more interesting story here is the pattern that cfg_select! exemplifies. Rust has a long history of features that lived in crates before they entered the language or standard library.

The ? operator emerged from the try! macro. Once-initialization patterns crystallized through lazy_static and once_cell before std::sync::OnceLock and std::sync::LazyLock arrived in stable Rust. std::backtrace::Backtrace was shaped by years of ecosystem-level experimentation. Now cfg_select! follows the same arc with cfg-if.

The advantage of this process is that the language team gets to observe real usage patterns before committing to a design. The ecosystem functions as a proving ground where the ergonomics and edge cases of an approach get stress-tested across thousands of real codebases. By the time a feature reaches stable, the design has usually been through substantial iteration.

The disadvantage is fragmentation during the transition. Some codebases use the crate, some use the standard library feature, and migration requires some coordination. For cfg_select!, the migration is mechanical: the semantics are identical to cfg-if and the syntax conversion is straightforward. The cfg-if crate will keep working through Rust’s edition compatibility guarantees, so there is no urgency. New code should reach for cfg_select! by default, and existing code can migrate at whatever pace makes sense.

If-let guards close the let chains story

The second feature in 1.95 is if-let guards in match expressions.

Rust 1.88 stabilized let chains, which allow binding patterns inside if conditions:

if let Some(conn) = get_connection() && conn.is_authenticated() {
    // conn is bound and authenticated here
}

This eliminated a common pattern of nested if let expressions and made conditional code that binds variables significantly more readable. What 1.88 left untouched was match guards. Before 1.95, match guard conditions were limited to boolean expressions. You could write if x > 0, but you could not pattern-match in the guard itself:

// Before 1.95: can filter, but cannot bind from a secondary lookup
match message {
    Message::Data(payload) if valid_payload(&payload) => {
        // to use get_metadata() result here, you need nested if-let
        // which changes control flow and prevents falling to _
    }
    _ => {}
}

If you needed to pattern-match on the result of a function call in the guard, you either moved the check into the arm body with a nested if let (which changes control flow, preventing fall-through to _), or you restructured the entire match around a helper function. Neither option is clean.

With 1.95, if-let guards work directly in match arms:

match message {
    Message::Data(payload) if let Some(meta) = get_metadata(&payload) => {
        process(payload, meta);
    }
    _ => {}
}

Both payload, bound from the arm pattern, and meta, bound from the guard, are available in the arm body. The guard is the single source of truth for whether the arm fires, and every binding it produces flows cleanly into the body.

The combination with let chains from 1.88 is also supported:

match event {
    Event::UserAction(action)
        if let Some(user) = lookup_user(action.user_id)
        && user.has_permission(action.kind) =>
    {
        execute_action(action, user);
    }
    _ => {}
}

This is the kind of pattern you write in a Discord bot event handler, a network protocol parser, or an authorization layer, anywhere the event type, the associated data, and a secondary lookup all need to align before you take an action. In those contexts, the previous limitation pushed complexity into arm bodies or helper functions that obscured the actual branching logic. If-let guards put the conditions back where they belong.

The feature was originally proposed in RFC 2294, which predates let chains by several years. The stabilization of let chains in 1.88 created the obvious question of why match guards couldn’t use the same syntax, and 1.95 resolves it.

What else is in 1.95

Beyond these two features, 1.95 continues the regular cadence of library stabilizations and compiler improvements that accompany every Rust release. The full release notes have the complete list. Library stabilizations accumulate quietly but the effect compounds: more APIs become available without external crates, the unsafe surface shrinks, and the gap between nightly experiments and stable code closes incrementally.

The cumulative picture

Looking at these two features together, 1.95 is a release about making Rust’s existing machinery more accessible. cfg_select! does not add new expressive power; it standardizes a pattern that has worked in userspace for a decade and gives it a syntax that reads naturally alongside the rest of the language. If-let guards extend pattern matching from if let expressions into match guards, the one remaining context where it was still missing.

Neither feature requires learning a new concept. Both make code that was already possible shorter and more direct. That kind of progress is worth taking seriously: update the idioms in new code, migrate existing uses when the opportunity arises, and let the crate dependency on cfg-if quietly retire.

Was this interesting?