#[cfg] if-then-else

I had an idea in the middle of the "Testing and mocking based on name conversion" thread that I think should maybe be discussed independently.

If you want to conditionally compile one item under some cfg condition, and a different item under the opposite of that cfg condition, right now you're forced to repeat the condition:

#[cfg_attr(test, path = "database_mock.rs")]
#[cfg_attr(not(test), path = "database_real.rs")]
mod database;

... and that's probably the nicest case. It can get much worse:

#[cfg(target_os = "macos")]
pub(crate) fn get_screen_resolution() {
   // several dozen lines of code
}
#[cfg(target_os = "linux")]
pub(crate) fn get_screen_resolution() {
   // several dozen lines of code
}
#[cfg(target_os = "windows")]
pub(crate) fn get_screen_resolution() {
   // several dozen lines of code
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
pub(crate) fn get_screen_resolution() {
   unimplemented!();
}

I think this points at a genuine missing language feature: compile-time cfg-based if-then-else chains.

cfg_attr can easily be extended to support if-then-else, we just need some notation, perhaps

#[cfg_attr(
   test                  => path = "database_mock.rs";
   target_os = "android" => path = "database_android.rs";
   /* otherwise */          path = "database_default.rs";
)]
mod database;

(semicolons separate the arms so you can still write multiple comma-separated attributes in each arm)

I don't, however, have a suggestion I 100% like for plain cfg applied to an item. The cfg-if crate lets you write, effectively, if cfg!(...) { ... } else if cfg!(...) { ... } else { ... } at file scope, and abstractly I think that's a nice notation, but I'm worried that people would immediately want to generalize it to arbitrary (const) controlling expressions and beyond, and maybe we don't want to let that genie out of its bottle?

cfg-if does seem to be very popular for this purpose.

We're talking about adding something like this in the standard library: cfg_match in std - Rust

4 Likes

The proposed cfg_match notation looks pretty solid to me. The strongest argument I can think of for built-in if cfg!(...) at top level instead is that it would get rid of a nesting level -- compare

cfg_match! {
    cfg(unix) => {
        fn foo() { /* unix specific functionality */ }
    }
    cfg(target_pointer_width = "32") => {
        fn foo() { /* non-unix, 32-bit functionality */ }
    }
    _ => {
        fn foo() { /* fallback implementation */ }
    }
}

vs

if cfg!(unix) {
    fn foo() { /* unix specific functionality */ }
} else if cfg!(target_pointer_width = "32") {
    fn foo() { /* non-unix, 32-bit functionality */ }
} else {
    fn foo() { /* fallback implementation */ }
}

...This maybe isn't the ideal example to show the tradeoffs involved, especially considering this is DRYer and also stable:

fn foo() {
    if cfg!(unix) {
        /* unix specific functionality */
    } else if cfg!(target_pointer_width = "32") {
        /* non-unix, 32-bit functionality */
    } else {
        /* fallback implementation */
    }
}

I'd expect existing users of cfg-if at top level are using it to conditionalize groups of items, and for that ... maybe the extra nesting level is more of a problem than it would be for variations of a single function? I am not actually familiar with any examples.

BTW, note that there's currently a proposal to simplify the syntax to drop the repeated cfg():

cfg_match! {
    unix => {
        /* unix specific functionality */
    }
    target_pointer_width = "32" => {
        /* non-unix, 32-bit functionality */
    }
    _ => {
        /* fallback implementation */
    }
}
2 Likes

This is unfortunately a somewhat interesting question to ask. How/why is cfg! made special here; is any macro expanding to a literal true/false okay?

Separately, how do we explain the different behavior of if cfg! at item scope and statement scope? Specifically, at statement scope if introduces a new scope (outside can't use names defined inside) and is checked at runtime (both branches must compile even if the controlling expression is a literal), but at item scope it chooses one arm to be compiled at the item scope and and discards the other arms. For this reason I prefer static if[1] or some other way to differentiate from standard if and allows it to be used in expression position, but that also makes the door to arbitrary controlling expressions much easier to open unless the syntax directly bakes in that it's a cfg expression.

So imo a std cfg_match! is the way to go for cfgs.


  1. People often call this const if, but I don't like that, as if const is valid and means something very different. A horrible but funny idea: if! ↩ī¸Ž

2 Likes

Having thought about it some more, I think the hypothetical module-scope if should accept an arbitrary const controlling expression. This is not harder to implement or explain than module-scope const data items. In fact, having module-scope if accept a different set of expressions from those that work as const data item initializers would be harder to explain.

You just did explain it! :wink:

Seriously, I don't see this as a problem. It does the most natural thing for it to do in each context; it's fine that the most natural thing happens to be different at module scope than statement scope. People learning the language will hear the explanation, go "oh, of course", and carry on.

Except that you can write items at statement scope. So what are people who want the cfg-discard behavior at statement scope supposed to do? Or as you said, it's possible to do if cfg! around a fn or inside the fn, and if the function signature is involved, it can be desirable to not duplicate it, but in that case the discarded if arms aren't allowed to use any names which are cfg gated.

For the same reason Rust uses if identically both for statement and expression position instead of having a separate ternary expression, imo it's just simpler to have a separate static if (or whatever syntax) for item position, rather than reuse the exact same if syntax.

1 Like