|
| 1 | +- Feature Name: `cfg_attribute_in_where` |
| 2 | +- Start Date: 2023-03-11 |
| 3 | +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) |
| 4 | +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Let's make it more elegant to conditionally compile trait bounds by allowing cfg-attributes directly in where clauses. |
| 10 | + |
| 11 | +# Motivation |
| 12 | +[motivation]: #motivation |
| 13 | + |
| 14 | +Currently, there is limited support for conditionally compiling trait bounds. Rust already supports using cfg-attributes in |
| 15 | +angle-bracketed bounds, so the following implementation is possible but unwieldy, and grows combinatorically with multiple |
| 16 | +independent compilation condition/bound pairs: |
| 17 | + |
| 18 | +```rust |
| 19 | +impl< |
| 20 | + #[cfg(something)] T: SomeRequirement, |
| 21 | + #[cfg(not(something))] T |
| 22 | +> SomeTrait<T> for Thing {} |
| 23 | +``` |
| 24 | + |
| 25 | +This also can't be used for bounds on associated types or other more complicated left-hand items that can only occur in full where bounds. |
| 26 | + |
| 27 | +Another somewhat-common approach is to create a dummy trait that conditionally branches and implement that, like so: |
| 28 | + |
| 29 | +```rust |
| 30 | +#[cfg(something)] |
| 31 | +trait Dummy: SomeRequirement {} |
| 32 | +#[cfg(something)] |
| 33 | +impl<T: SomeRequirement> Dummy for T {} |
| 34 | +#[cfg(not(something))] |
| 35 | +trait Dummy {} |
| 36 | +#[cfg(not(something))] |
| 37 | +impl<T> Dummy for T {} |
| 38 | + |
| 39 | +impl<T: Dummy> SomeTrait<T> for Thing {} |
| 40 | +``` |
| 41 | + |
| 42 | +However, this boilerplate does not grow well for multiple conditionally-compiled requirements, becoming rather soupy even at N = 2: |
| 43 | + |
| 44 | +```rust |
| 45 | +#[cfg(something_a)] |
| 46 | +trait DummyA: SomeRequirementA {} |
| 47 | +#[cfg(something_a)] |
| 48 | +impl<T: SomeRequirementA> DummyA for T {} |
| 49 | +#[cfg(not(something_a))] |
| 50 | +trait DummyA {} |
| 51 | +#[cfg(not(something_a))] |
| 52 | +impl<T> DummyA for T {} |
| 53 | + |
| 54 | +#[cfg(something_b)] |
| 55 | +trait DummyB: SomeRequirementB {} |
| 56 | +#[cfg(something_b)] |
| 57 | +impl<T: SomeRequirementB> DummyB for T {} |
| 58 | +#[cfg(not(something_b))] |
| 59 | +trait DummyB {} |
| 60 | +#[cfg(not(something_b))] |
| 61 | +impl<T> DummyB for T {} |
| 62 | + |
| 63 | +impl<T: DummyA + DummyB> SomeTrait<T> for Thing {} |
| 64 | +``` |
| 65 | + |
| 66 | +Other alternative ways of achieving this also exist, but are typically macro heavy and difficult to implement or check. Importantly, this |
| 67 | +functionality already exists in the language, but quickly grows out of reasonable scope to ergonomically implement. |
| 68 | + |
| 69 | +# Guide-level explanation |
| 70 | +[guide-level-explanation]: #guide-level-explanation |
| 71 | + |
| 72 | +Where clauses can use cfg-attributes on individual bounds, like so: |
| 73 | + |
| 74 | +```rust |
| 75 | +impl<T> SomeTrait<T> for Thing |
| 76 | +where |
| 77 | + #[cfg(something_a)] T: SomeRequirementA, |
| 78 | + #[cfg(something_b)] T: SomeRequirementB, |
| 79 | +{} |
| 80 | +``` |
| 81 | +or on functions, including multiple cfg-attributes on a single bound: |
| 82 | +```rust |
| 83 | +fn some_function<T>(val: &T) |
| 84 | +where |
| 85 | + #[cfg(something_a)] |
| 86 | + T: SomeRequirementA, |
| 87 | + #[cfg(something_b)] |
| 88 | + #[cfg(not(something_a))] |
| 89 | + #[cfg(target_os(some_os))] |
| 90 | + T: SomeRequirementB, |
| 91 | +{} |
| 92 | +``` |
| 93 | +and in other situations where where clauses apply. |
| 94 | + |
| 95 | +During compilation, all cfg-attributes on a where bound are evaluated. If the evaluation result is false, then the bound in question is not |
| 96 | +compiled and the bound does not apply to the given type. This may cause errors if code that relies on those bounds is not itself also |
| 97 | +conditionally compiled. For anyone familiar with cfg-attributes already, this should behave similarly to how they are used in, say, struct |
| 98 | +fields or on function signatures. |
| 99 | + |
| 100 | +# Reference-level explanation |
| 101 | +[reference-level-explanation]: #reference-level-explanation |
| 102 | + |
| 103 | +In positions that accept where clauses, such as trait implementations and function signatures, individual clauses can now be decorated with |
| 104 | +cfg-attributes. The cfg-attribute must be on the left hand of the colon (e.g. `#[cfg(...]) T: Foo` rather than `T: #[cfg(...)] Foo`) and |
| 105 | +apply for that one bound, up to the comma or end of the where section. Each bound collection will be conditionally compiled depending on the |
| 106 | +conditions specified in the cfg arguments. Note that this may cause a where clause to conditionally compile as having no bound entries |
| 107 | +(i.e. an empty where clause), but this has been allowed in Rust since 1.16 and already occurs from time to time when using macros. |
| 108 | + |
| 109 | +# Drawbacks |
| 110 | +[drawbacks]: #drawbacks |
| 111 | + |
| 112 | +As with any feature, this adds complication to the language and grammar. In general, conditionally compiled trait bounds can create |
| 113 | +unintended interactions or constraints on code based on compilation targets or combinations of features. The drawbacks to this proposed |
| 114 | +code path already apply to the existing workarounds used to achieve the same functionality. |
| 115 | + |
| 116 | +# Rationale and alternatives |
| 117 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 118 | + |
| 119 | +This functionality can already be achieved in Rust, but not elegantly, and without a clear relationship between the written code and its |
| 120 | +intent. The two main alternatives are dummy traits and cfg-attributes in angle-bracketed bounds. Compared to using dummy traits, adding a |
| 121 | +cfg-attribute in a where clause makes the intent immediately local and more directly associates it with the piece of code it's intended to |
| 122 | +control. Compared to using cfg-attributes in angle-bracketed bounds, adding a cfg-attribute in a where clause means each bound can be |
| 123 | +individually toggled without the need for combinatoric combinations of conditions, and allows conditional compilation on bounds with |
| 124 | +nontrivial item paths. |
| 125 | + |
| 126 | +The need for conditionally compiling trait bounds can arise in applications with different deployment targets or that want to release |
| 127 | +builds with different sets of functionality (e.g. client, server, editor, demo, etc.). It would be useful to support cfg-attributes |
| 128 | +directly here without requiring workarounds to achieve this functionality. Macros, proc macros, and so on are also ways to conditionally |
| 129 | +compile where clauses, but these also introduce at least one level of obfuscation from the core goal. Finally, traits can be wholly |
| 130 | +duplicated under different cfg-attributes, but this scales poorly with both the size and intricacy of the trait and the number of |
| 131 | +interacting attributes (which may grow combinatorically), and can introduce a maintenance burden from repeated code. |
| 132 | + |
| 133 | +# Prior art |
| 134 | +[prior-art]: #prior-art |
| 135 | + |
| 136 | +I'm not aware of any prior work in adding this to the language. Languages with preprocessors could support this with something like: |
| 137 | + |
| 138 | +```rust |
| 139 | +impl<T> SomeTrait<T> for Thing |
| 140 | +where |
| 141 | +#ifdef SOMETHING_A |
| 142 | + T: SomeRequirementA |
| 143 | +#endif |
| 144 | +{} |
| 145 | +``` |
| 146 | +but that's not the way I would expect Rust to provide this kind of functionality. |
| 147 | + |
| 148 | +# Unresolved questions |
| 149 | +[unresolved-questions]: #unresolved-questions |
| 150 | + |
| 151 | +In theory, I don't see any harm in cfg-attributes decorating individual bounds on the right hand side of the colon. Is it worth adding that |
| 152 | +potential feature as well? Personally, I don't see it as being worth the added complexity given that you can have multiple individual bound |
| 153 | +declarations for the same item. Doing so would also create an inconsistency, given that this isn't currently allowed in angle-bracketed |
| 154 | +bounds either. |
| 155 | + |
| 156 | +# Future possibilities |
| 157 | +[future-possibilities]: #future-possibilities |
| 158 | + |
| 159 | +Conditional bounds on where clauses could also be used for [trivial bounds](https://github.com/rust-lang/rust/issues/48214). I don't believe |
| 160 | +any extra support would be needed here since the conditional compilation would occur at the grammar level rather than the type level. |
0 commit comments