Question about "features should be additive"

In this section of The Cargo Book, it says that: A consequence of this is that features should be *additive* .

It gives an example of no_std:

"
For example, if you want to optionally support no_std environments, do not use a no_std feature. Instead, use a std feature that enables std .
"

While I understand the intention, I'm wondering if that's always the best choice in practice.

Using the same example, if a library foo is used in std environment for 90% cases, and also supports no_std for the rest 10% cases, I feel that having a feature of no_std is more intuitive, simpler for most users especially if the user has default-features = false.

I think there is a kind of “natural expectation” that a feature represents something special or optional rather than the most common configuration. In this example, I feel defining std as a feature goes against such intuition.

What do you think?

Features must be additive. If you can design them to be intuitive too, that's nice, but non-additive features break people's programs.

However, that doesn't mean that there must never be a no_std feature. If a library has functionality that normally requires std but can instead pull in other libraries to substitute perfectly for std, then a feature enabling that would be additive.

The case where you must write a std feature and not a no_std feature is when your library has items that can only exist when std is available. Then, it is okay to have a std feature that adds those items, because no dependent can be broken by that addition, but not okay to have a no_std feature that removes those items, because that could break dependents.

7 Likes

In practice, a lib with a std feature is almost always a default feature, so you only have to think about it when you're disabling the default features, which feels pretty intuitive to me.

Thanks! I think I understand it more now. It seems there is no perfect way to do it (for a new feature foo):

  • Following the "additive" rule, and set foo=true as default. If a user was using default_features = false, it would be a breaking change for them.

  • Not to follow the "additive" rule and define no_foo feature that is false by default. If the dependency tree has 2 references to the same lib, one being with default settings, another being setting no_foo, the final result is no_foo and might break the final program.

I'll look more into the case at hand and consider the trade-off.

The reason is that library crates can't disable any features. They have no control over which features are left off (your false in Cargo.toml has no power), because some crate somewhere else in the dependency tree can require enabling a feature in your crate or one of your dependencies. In Cargo's design enabling always wins over not enabling.

Features are unified across the entire dependency tree, so if one dependency enables a feature, the feature is enabled for everyone else too.

If you tried to rely on not having no_std set on one of your dependencies, then it would be broken at distance by any other crate anywhere else that did ask for no_std on the same dependency. It doesn't matter which way is more likely, in Cargo one way works and the other doesn't.

3 Likes