Skip to content

Commit 188cc17

Browse files
authored
Merge pull request #3399 from recatek/cfg-attribute-in-where
Allow cfg-attributes in where clauses
2 parents 33cdbf4 + 57f1df9 commit 188cc17

File tree

1 file changed

+162
-0
lines changed

1 file changed

+162
-0
lines changed

text/3399-cfg-attribute-in-where.md

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
- Feature Name: `cfg_attribute_in_where`
2+
- Start Date: 2023-03-11
3+
- RFC PR: [rust-lang/rfcs#3399](https://github.com/rust-lang/rfcs/pull/3399)
4+
- Rust Issue: [rust-lang/rust#115590](https://github.com/rust-lang/rust/issues/115590)
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 trait 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+
applies to 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+
* rustfmt is supposed to be able to format the where clause somehow, do we expect it to (try to) put the attribute on the same line, or would it always prefer the attribute on separate lines?
157+
158+
# Future possibilities
159+
[future-possibilities]: #future-possibilities
160+
161+
Conditional bounds on where clauses could also be used for [trivial bounds](https://github.com/rust-lang/rust/issues/48214). I don't believe
162+
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

Comments
 (0)