← Back to index
PR #3532Work-in-progress preview from an open pull request.View on GitHub ↗
REVIEW
#3532

cfg_attr_in_tuple_tys_and_pats

Authorrecatek
CreatedNov 23 2023
UpdatedJun 27 2026
Rust Issue

Let's make it more elegant to conditionally compile tuple type declarations and pattern matches by allowing (outer) cfg-attributes directly on their elements.

Motivation

Consistency

Currently, there is limited support for conditionally compiling tuple type declarations:

type ConditionalTuple = (u32, i32, #[cfg(feature = "foo")] u8);
error: expected type, found `#`
 --> <source>:1:36
  |
1 | type ConditionalTuple = (u32, i32, #[cfg(feature = "foo")] u8);
  |                                    ^ expected type

As with RFC #3399, some workarounds exist, but they can result in combinatorial boilerplate:

// GOAL: 
// type ConditionalTuple = (
//     u32, 
//     i32, 
//     #[cfg(feature = "foo")] u8,
//     #[cfg(feature = "bar")] i8,
// );

// CURRENT:
#[cfg(all(feature = "foo", feature = "bar"))]
type ConditionalTuple = (u32, i32, u8, i8);
#[cfg(all(feature = "foo", not(feature = "bar")))]
type ConditionalTuple = (u32, i32, u8);
#[cfg(all(not(feature = "foo"), feature = "bar"))]
type ConditionalTuple = (u32, i32, i8);
#[cfg(all(not(feature = "foo"), not(feature = "bar")))]
type ConditionalTuple = (u32, i32);

Rust already supports per-element cfg-attributes in tuple initialization. The following is legal Rust code and functions as expected, even though the resulting type of x can't be expressed very easily:

pub fn main() {
    let x = (1u32, 4i32, #[cfg(all())] 23u8);
    println!("{}", x.2) // Output: 23
}

Similarly, cfg-attributes are permitted on types in tuple structs, like so:

pub struct SomeStruct(u32, #[cfg(feature = "foo")] bool);

So it makes sense to support it in regular tuple type declaration as well.

Use Cases

While structs support cfg-attributes on their members, tuples serve an important purpose in a number of applications that can't easily be replicated with structs. One common example is for achieving variadic-like behavior for constructing and accessing struct-of-array (SoA) data structures. These data structures break large data blocks into modular data components in individual contiguous memory blocks for reusable composition and optimizations via SIMD and improved cache behavior. This is especially prevalent in Entity Component System libraries like bevy and hecs. For example, to perform a world query in hecs, the user constructs an iterator using a type-tuple like so:

for (id, (number, &flag)) in world.query_mut::<(&mut i32, &bool)>() {
  if flag { *number *= 2; }
}

Tuples have a number of unique advantages in this paradigm. For one, they avoid boilerplate due to their ability to be anonymously constructed on the fly. Additionally, tuples can be concatenated and joined (e.g. via the tuple crate). This allows more advanced ECS libraries and other similar tools to provide support for pre-determined bundles of components, or use tuple nesting to group logic and functionality. One could theoretically define functions like query_mut1<T0>, query_mut2<T0, T1>, and so on, but the ergonomics of the tuple approach win out in practice.

In this situation, cfg-attributes come into play when building ECS archetypes (a pre-determined collection of components for a type of entity) for different platforms or deployment targets. Say for example that we were creating a multiplayer asteroids game in an Entity Component System. If we wanted to statically define our archetypes at compile-time (as is the case in gecs), it might look something like this:

type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    SpriteComponent,
    AudioComponent,
)>;

Since this is a multiplayer game, we may want some components to exist solely on the server or on the client, both for security reasons and also for optimization or performance reasons. The sprite and audio components for example serve no purpose on the server as the server does not render graphics or play audio. In games in other languages, it is common practice to use conditional compilation to avoid putting code in various build targets that serve no purpose, waste resources, or potentially leak information to cheaters. So in this case we will restrict these two components to the client feature, like so:

type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    #[cfg(feature = "client")] SpriteComponent,
    #[cfg(feature = "client")] AudioComponent,
)>;

Additionally, we need some components to handle serializing the network state, performing dead reckoning, and sending that information to the client from the server. So we will add a StateStorageComponent and a DeltaCompressionComponent, and restrict those to the server, since the client does not perform these calculations and we want to avoid giving clients this information in order to help confound cheaters.

type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    #[cfg(feature = "client")] SpriteComponent,
    #[cfg(feature = "client")] AudioComponent,
    #[cfg(feature = "server")] StateStorageComponent,
    #[cfg(feature = "server")] DeltaCompressionComponent,
)>;

Finally, we want some debug information for diagnosing physics and damage calculation issues. We build a component to store this intermediate data, but we don't want to ship it in the final game because it's just for aid in development. We'll create a DebugDrawComponent and add it to our ship archetype as well, but only when the game is built in editor mode because it's quite expensive to do these extra calculations and draw debug information every frame.

type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    #[cfg(feature = "client")] SpriteComponent,
    #[cfg(feature = "client")] AudioComponent,
    #[cfg(feature = "server")] StateStorageComponent,
    #[cfg(feature = "server")] DeltaCompressionComponent,
    #[cfg(feature = "editor")] DebugDrawComponent,
)>;

This represents our archetype with the various common and situational components based on its build and deployment target. With this decoration each component is decorated with the context in which it appears, and requires no inference or indirection via macros to generate or read. By comparison, here is how this would be written in Rust today, keeping in mind that a build could be any combination of client, server, and editor for development and debugging purposes (akin to Unreal Engine's "play in editor" feature):

#[cfg(all(feature = "client", feature = "server", feature = "editor"))]
type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    SpriteComponent,
    AudioComponent,
    StateStorageComponent,
    DeltaCompressionComponent,
    DebugDrawComponent,
)>;

#[cfg(all(not(feature = "client"), feature = "server", feature = "editor"))]
type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    StateStorageComponent,
    DeltaCompressionComponent,
    DebugDrawComponent,
)>;

#[cfg(all(feature = "client", not(feature = "server"), feature = "editor"))]
type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    SpriteComponent,
    AudioComponent,
    DebugDrawComponent,
)>;

#[cfg(all(not(feature = "client"), not(feature = "server"), feature = "editor"))]
type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    DebugDrawComponent,
)>;

#[cfg(all(feature = "client", feature = "server", not(feature = "editor")))]
type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    SpriteComponent,
    AudioComponent,
    StateStorageComponent,
    DeltaCompressionComponent,
)>;

#[cfg(all(not(feature = "client"), feature = "server", not(feature = "editor")))]
type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    StateStorageComponent,
    DeltaCompressionComponent,
)>;

#[cfg(all(feature = "client", not(feature = "server"), not(feature = "editor")))]
type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
    SpriteComponent,
    AudioComponent,
)>;

#[cfg(all(not(feature = "client"), not(feature = "server"), not(feature = "editor")))]
type ShipArchetype = EcsArchetype<(
    TransformComponent,
    VelocityComponent,
    PhysicsComponent,
    ColliderComponent,
    EngineComponent,
    HealthComponent,
    WeaponComponent,
    EnergyComponent,
)>;

This would likely need to be generated via macro in practice, and the macro itself would have to parse the cfg-attributes to produce these combinatorial outputs. However, macros aren't an easy fix in all positions where tuples are supported (e.g. as type arguments), and so even with macros this would create levels of indirection and require alias definitions. The hecs query example above could not easily have an element conditionally gated via a macro without first declaring an alias for that query's tuple type outside of the position where the query iteration occurs. This is because doing so would likely require the macro to be able to generate code outside of its immediate context to function (i.e. to branch based on each cfg-attribute involved).

In addition to supporting cfg-attributes in tuple declarations, this RFC proposes supporting these attributes in pattern matching as a proper counterpart. For example:

let (a, #[cfg(something)] b) = my_tuple;

or

struct MyStruct(i32, #[cfg(something)] u32); // Already supported

fn foo(x: MyStruct) {
    let MyStruct(a, #[cfg(something)] b) = x; // Not yet supported
}

or

match my_tuple {
    (val, #[cfg(something)] Some(other)) => { ... },
}

To continue the client-server analogy above, in a hecs-like ECS query, it is useful to conditionally define components on a client- or server-only basis. For example, if the server needed to run additional logic and store additional state when an object updates its movement, one might accomplish this like so:

type MovementQuery = (Position, Rotation, #[cfg(server)] Authority);

fn do_update(ecs: &mut Ecs) {
    for components in ecs.query::<MovementQuery>().iter_mut() {
        let (position, rotation, #[cfg(server)] authority) = components;
        let _output = update_movement(position, rotation);
        
        #[cfg(server)]
        update_authority(authority, position, rotation, _output);
    }
}

Permitting cfg attributes in both the tuple definition, and in the pattern matching to extract tuple elements, allows code like this to conditionally branch without the risk of combinatorial explosion as above.

Guide-level explanation

Tuple type declarations can use cfg-attributes on individual elements, like so:

type MyTuple = (
    SomeTypeA,
    #[cfg(something_a)] SomeTypeB,
    #[cfg(something_b)] SomeTypeC,
)

and in other situations where tuple types are declared, such as in function arguments. These will conditionally include or exclude the type in that tuple (affecting the tuple's length) based on the compile-time evaluation result of each #[cfg] predicate.

Similarly, cfg-attributes can be used in pattern matching, like so:

let (a, #[cfg(something)] b) = my_tuple;

and

match my_tuple {
    (val, #[cfg(something)] Some(other)) => { ... },
}

What about other attributes?

While this RFC is focused on #[cfg], for the sake of clarification, the following attributes are also allowed in these new positions:

  • cfg_attr (in addition to cfg)
  • Lint-level attributes (because they are allowed in all attribute positions)
  • diagnostic attributes (because they are allowed in all attribute positions, but generate a warning)
  • Tool attributes (which are not restricted by rustc)

On the other hand, the following attributes are not allowed in these new positions:

  • The doc attribute and doc comments

Reference-level explanation

This RFC proposes changing the syntax of the TupleType (See [type.tuple.syntax]) to include OuterAttribute* before each occurrence of Type. These attributes can decorate each individual type (up to the comma or closing paren). Similarly, for pattern matching, this RFC proposes adding OuterAttribute* before each Pattern in TuplePattern (See [patterns.tuple.syntax]) and TupleStructPattern (See [patterns.tuple-struct.syntax]). This would work similarly to the OuterAttribute* in StructPatternField (See [patterns.struct.syntax]),

In practice, at least within the scope of this RFC, only cfg-attributes need to be supported in these new OuterAttributes.

Drawbacks

As with any feature, this adds complication to the language and grammar. Conditionally compiling tuple type elements can be a semver breaking change, but not any more than with the already existing workarounds.

Rationale and alternatives

(See RFC #3399 for a similar write-up.)

The need for conditionally compiling tuple types can arise in applications with different deployment targets or that want to release builds with different sets of functionality (e.g. client, server, editor, demo, etc.). It would be useful to support cfg-attributes directly here without requiring workarounds to achieve this functionality. Macros, proc macros, and so on are also ways to conditionally compile tuple types, but these also introduce at least one level of obfuscation from the core goal and can't be used everywhere a tuple can be. Finally, tuples can be wholly duplicated under different cfg-attributes, but this scales poorly with both the size and intricacy of the tuple and the number of interacting attributes (which may grow combinatorically), and can introduce a maintenance burden from repeated code.

It also makes sense in this instance to support cfg-attributes here because they are already supported in this manner for tuple initialization and for tuple struct declaration, as well as for individual fields in pattern matching on structs.

Prior art

I'm not aware of any prior work in adding this to the language. Other forms of this kind of cfg-attribute support exist elsewhere in the language. For example in tuple structs (illustrated above), and in pattern matching on non-tuple structs, where matched fields can already be conditionally gated, like so:

struct MyStruct {
    a: u32,
    #[cfg(true)]
    b: u32,
}

fn foo(x: MyStruct) {
    let MyStruct{
        a,
        #[cfg(true)]
        b,
    } = x;
}

Unresolved questions

There are currently no unresolved questions for this RFC.

Future possibilities

I believe this change is relatively self-contained, though I also think it's worth continuing to look for additional places where support for cfg-attributes makes sense to add. Conditional compilation is very important, especially in some domains, and requiring workarounds and additional boilerplate to support it is not ideal. A more detailed enumeration of inconsistencies with cfg-attributes and comma-terminated fragments can be found on this HackMD page. If this RFC is accepted, only two reasonable use cases for cfg-attributes on comma-terminated fragments would remain uncovered.

Appendix

State of comma-terminated fragments and cfg-attributes in Rust

This is, to my current knowledge, an exhaustive list of every comma-terminated fragment type (explicit or implicit) in the language, and whether or not it can be decorated with a cfg-attribute. Note that macros are excluded from this list because they would just try to match on the cfg-attribute syntax, and I don't see a strong reason to try to change that behavior.

Can be decorated

Function arguments in signatures

fn foo(#[cfg(all())] a: u32) {} // OK

Function arguments for invocation

foo(#[cfg(all())] 1); // OK

Function arguments in function pointers

fn foo(f: fn(i32, #[cfg(all())] i32)) {} // OK

Closure arguments

let a = |#[cfg(all())] a: u32| {}; // OK

Generic type arguments in signatures and declarations

fn foo<#[cfg(all())] T>(t: T) {} // OK

struct Foo<#[cfg(all())] T> { // OK
    t: T,
}

Arguments for impl (but oddly not for the struct argument, see below)

impl<#[cfg(all())] T> Foo<T> { } // OK

Associated type arguments

trait Foo {
    type Inner<#[cfg(all())] 'a>; // OK
}

impl Foo for Bar {
    type Inner<#[cfg(all())] 'a> = &'a u32; // OK
}

Struct fields (both types) and union fields:

struct Foo {
    #[cfg(all())] t: u32, // OK
}

struct Foo (
    #[cfg(all())] u32, // OK
);

#[repr(C)]
union Foo {
    a: u32,
    #[cfg(all())] b: f32, // OK
}

Struct initialization (both types) and tuple initialization

let x = Foo {
    #[cfg(all())] t: 1u32, // OK
};

let x = Foo(
    #[cfg(all())] 1u32, // OK
);

let x = (#[cfg(all())] 1u32,); // OK

Enums and match

enum Foo {
    A,
    #[cfg(all())] B, // OK
}

fn foo(a: Foo) -> u32 {
    match a {
        Foo::A => 1,
        #[cfg(all())] Foo::B => 2, // OK
    }
}

Arrays

fn foo() {
    let x = [0, #[cfg(all())] 1][0]; // OK
}

Where clauses (in progress, see accepted RFC 3399 and tracking issue #115590):

impl<T> SomeTrait<T> for Foo
where
    #[cfg(all())] T: SomeRequirementA, // OK (Eventually)
{}

Can not be decorated

Tuple type declaration (proposed in this RFC 3532):

type Foo = (#[cfg(all())] u32,); // BAD

Pattern matching (proposed in this RFC 3532):

fn bar(v: (u32, u32)) {
    let (a, #[cfg(all())] b) = v; // BAD
}

Explicit generic arguments in invocation:

foo::<#[cfg(all())] u32>(1); // BAD

Struct generic arguments, including in impl

struct Bar {
    f: Foo<#[cfg(all())] u32>, // BAD
}

impl<#[cfg(all())] T> Foo<#[cfg(all())] T> { }
//   ^^^^^^^^^^^^^ OK     ^^^^^^^^^^^^^ BAD

Trait(...) syntax (FnOnce, FnMut, etc.)

fn foo(f: fn(i32, #[cfg(false)] i32)) {} // OK

fn bar(f: impl FnOnce(i32, #[cfg(false)] i32)) {} // BAD

Braced multi-use statements (this is a little silly)

use std::{ptr, #[cfg(all())] mem}; // BAD