How to use concat_idents macro

The concat_idents macro is removed; use the macro_metavar_expr_concat feature with ${concat(...)} syntax instead.

When static names fall short

You are writing a declarative macro that needs to generate a family of related types. You want to take a user-provided name like User and automatically create UserConfig, UserCache, and UserValidator. You reach for the old concat_idents! macro, only to find the compiler complaining that it does not exist. The macro was removed from the standard library. The replacement lives inside the macro expansion engine itself, using a syntax that looks like template interpolation.

How the replacement actually works

Rust macros operate on tokens, not strings. When you write a macro, you are handing the compiler a set of rules for rearranging code before the type checker runs. The old concat_idents! tried to glue identifier tokens together, but it clashed with Rust's hygiene system and macro expansion order. It also required a separate macro invocation inside the generated code, which broke the single-pass expansion model.

The modern approach uses metavariable expressions. You wrap the concatenation logic in ${...} and place it directly where an identifier belongs. The compiler evaluates the expression during macro expansion and injects the resulting token into the output stream. Think of it like a mail-merge engine that runs before the document is printed. You provide the template, the compiler fills in the blanks, and the final code looks like you typed it by hand. The feature gate macro_metavar_expr_concat unlocks this behavior. It is currently unstable, which means you need a nightly compiler to use it.

Keep your feature gates visible. Place #![feature(...)] at the very top of your crate root. Readers scanning your code need to see the nightly dependency immediately.

Minimal example

#![feature(macro_metavar_expr_concat)]

macro_rules! create_struct {
    ($name:ident) => {
        // The ${concat(...)} block runs during macro expansion.
        // It stitches the literal "First" to the provided $name.
        // The result becomes a single identifier token.
        pub struct ${concat(First, $name)};
    };
}

fn main() {
    create_struct!(Thing);
    // The compiler now sees: pub struct FirstThing;
    let _instance = FirstThing;
}

Trust the expansion engine. If the tokens line up, the rest of the compiler treats it as ordinary code.

What happens during compilation

When the compiler encounters create_struct!(Thing), it does not immediately look for a type named FirstThing. It first expands the macro. The $name:ident matcher captures Thing. The ${concat(First, $name)} expression evaluates to the token FirstThing. The macro engine replaces the entire block with pub struct FirstThing;. Only after this substitution does the regular parser take over. The type checker sees a standard struct declaration.

The dynamic naming happens entirely at compile time. No runtime overhead exists. No string allocation occurs. The generated code is identical to handwritten Rust. The key difference is hygiene. The generated identifier inherits the hygiene context of the macro definition, not the call site. This prevents accidental name collisions with variables or types that happen to exist in the caller's scope. The compiler tracks where each token originated, and ${concat(...)} respects those boundaries.

If you need to see exactly what the compiler generates, run cargo expand or use cargo rustc -- -Z macro-backtrace. Debugging macro output becomes straightforward once you know where to look.

Realistic usage pattern

Real code rarely generates a single struct. You usually need a coordinated set of items. Here is a macro that generates a configuration module with getters, setters, and a reset function.

#![feature(macro_metavar_expr_concat)]

macro_rules! generate_config {
    ($field:ident) => {
        // Generate a getter function that returns a reference.
        // The function name combines "get_" with the field name.
        pub fn ${concat(get_, $field)}() -> &'static str {
            "default_value"
        }

        // Generate a setter function that takes a string slice.
        // The function name combines "set_" with the field name.
        pub fn ${concat(set_, $field)}(value: &str) {
            // In real code, you would update a static or global state here.
            // This placeholder just demonstrates the expansion.
            let _ = value;
        }

        // Generate a reset function that clears the configuration.
        // The function name combines "reset_" with the field name.
        pub fn ${concat(reset_, $field)}() {
            ${concat(set_, $field)}("default_value");
        }
    };
}

fn main() {
    generate_config!(timeout);
    // Expands to get_timeout, set_timeout, and reset_timeout.
    get_timeout();
    set_timeout("5000ms");
    reset_timeout();
}

Notice how ${concat(set_, $field)} appears inside the reset function body. The macro engine evaluates it twice, once for the definition and once for the call. Both evaluations produce the exact same token. The compiler never sees mismatched names.

Keep generated names predictable. If your macro generates get_ and set_ prefixes, document that pattern. Unexpected naming conventions make debugging macro output a nightmare.

Pitfalls and compiler friction

The syntax works, but it comes with constraints. The feature is unstable. If you compile on stable Rust, the compiler rejects the crate with E0635 (unknown feature macro_metavar_expr_concat). You must use nightly. Even on nightly, macro hygiene can trip you up. The generated identifier inherits the hygiene context of the macro definition, not the call site. If you generate a name that collides with an existing item in the caller's scope, the compiler throws E0428 (the name is defined multiple times).

Another common trap is trying to concatenate non-identifier tokens. ${concat(...)} only produces identifiers. If you need to build string literals or numeric constants, this syntax will fail. The compiler will emit a mismatched token type error during expansion. Use the standard concat! macro for strings and numbers. It operates at compile time and works on stable Rust.

Macro expansion also has depth limits. If you nest ${concat(...)} inside recursive macros without a clear base case, the compiler will halt with a stack overflow or expansion limit error. Keep your macro rules flat. Delegate complex logic to helper functions or trait implementations.

Treat macro expansion as a compile-time contract. If it breaks, the error message points to the macro definition, not the call site. Read the backtrace carefully.

Choosing the right tool

Use ${concat(...)} when you are writing a declarative macro on nightly and need to generate identifiers dynamically. Reach for the paste crate when you must support stable Rust and need the same identifier concatenation behavior. Stick to static names when the generated items do not actually depend on user input. Avoid dynamic identifiers when you can achieve the same goal with generics or trait implementations. The macro system is powerful, but it is not a substitute for type-driven design.

Where to go next