Quick tip: the #[cfg_attr] attribute

Today I want to remind people of #[cfg_attr]: it’s a really handy way of reducing code duplication in some situations, and not enough people know about.

What is #[cfg_attr]?

It’s just like #[cfg], but for attributes. Just as #[cfg(condition)] allows you to only compile the thing it decorates if the condition is true, #[cfg_attr(condition, attribute)] allows you to only add the attribute to the thing it decorates if the condition is true. That is, if the condition is true, it’s equivalent to #[attribute], and if the condition is false, it vanishes into thin air.

Real‐world examples

Enabling nightly features in certain situations

In anymap, I have a nightly Cargo feature which, if enabled, implements some additional features and performance optimisations (like using a more efficient hasher on the hash map) that are not yet possible on stable rustc. Those require a couple of extra gated features, but I only want the #![feature(core, std_misc)] crate attribute to exist if the nightly Cargo feature is enabled. I can do that:

#![cfg_attr(feature = "nightly", feature(core, std_misc))]

This might also be handy for things like tests:

#![cfg_attr(test, feature(test, anything_else_only_relevant_for_tests))]

Special attributes for bootstrapping rustc

A number of times I have noticed duplicated items, one #[cfg(stage0)] and one #[cfg(not(stage0))], differing only in attributes; this is not necessary. I’ll take a current example:

Before: duplication just for one attribute’s difference.

/// `PhantomFn` is a deprecated marker trait that is no longer needed.
#[lang="phantom_fn"]
#[stable(feature = "rust1", since = "1.0.0")]
#[deprecated(since = "1.0.0", reason = "No longer needed")]
#[cfg(stage0)]
pub trait PhantomFn<A:?Sized,R:?Sized=()> {
}

/// `PhantomFn` is a deprecated marker trait that is no longer needed.
#[stable(feature = "rust1", since = "1.0.0")]
#[deprecated(since = "1.0.0", reason = "No longer needed")]
#[cfg(not(stage0))]
pub trait PhantomFn<A:?Sized,R:?Sized=()> {
}

After: yay! no duplicating the entire thing!

/// `PhantomFn` is a deprecated marker trait that is no longer needed.
#[cfg_attr(stage0, lang = "phantom_fn")]
#[stable(feature = "rust1", since = "1.0.0")]
#[deprecated(since = "1.0.0", reason = "No longer needed")]
#[cfg(stage0)]
pub trait PhantomFn<A:?Sized,R:?Sized=()> {
}

Bonus: doc comments are truly sugar

Dynamic doc comments is a non‐obvious area in which #[cfg_attr] can be used.

First of all you must understand that these two are precisely the same in all ways that matter, because the former desugars to the latter during parsing:

/// This is the first line of the documentation.
///
/// This is another line.
#[doc = " This is the first line of the documentation."]
#[doc = ""]
#[doc = " This is another line."]

(Multiple #[doc] attributes are fine; they are joined together with line breaks between to form the final doc comment.)

This can be combined with #[cfg_attr] to allow conditional documentation, like adding a part to the documentation if a feature is enabled:

/// This trait does all sorts of cool things.
/// If the crate is built with the `foo` feature enabled,
/// it also does this other whizzy thing!
///
/// This documentation was built with the `foo` feature
#[cfg_attr(feature = "foo", doc = " **enabled**.")]
#[cfg_attr(not(feature = "foo"), doc = " **disabled** (tough luck).")]
///
/// Now we go onto some more documentation.
///
#[cfg_attr(feature = "bar", doc = "\
 # Barness

 You also have barness available. This is really cool stuff that lets you go
 whizzety bang pop. In real life I’d be dedicating a few paragraphs and a code
 sample or two to this. Because this isn’t real life, I’ll just ask that you
 please don’t confuse *barness* with *baroness*, because that would make me sad
 because you would be wrong.")]
#[cfg_attr(not(feature = "bar"), doc = "\
 You know, if you compiled with the `bar` feature enabled, you’d have a lot
 more fun. See [this helpful documentationy link that I will have] for details
 on how to accomplish that.")]
trait Traitor { }

When built, the documentation will then announce whether it was built with the foo feature enabled or disabled, which might be handy. And a new section will be added if the bar feature is enabled, or a note on what you’re missing out on and maybe how to enable it if you don’t have it enabled. There, four versions of the documentation without the need to duplicate even a single vast swathe of code!

Note also how when we manually write #[doc] attributes we put a single leading space at the start of the string; this is because /// Foo desugars to #[doc = " Foo"]; if all the lines start with a space, it will be trimmed from them all, but if not then such lines will have a leading space, which might mess up your Markdown formatting of things like headings marked with leading hashes.

I will admit that dynamic doc comments isn’t going to be completely useful everywhere, for useful applications of it aren’t going to be very common and if people use only one centralised documentation source they’ll miss out on getting the special version that just suits their use case, but as we get better tooling I think it might become more appropriate. And even if it’s not so very useful (hey, it’s bonus material, it doesn’t need to be so very useful), it’s definitely fun.

Super bonus: adding macros into the mix!

Imagine that the trait bounds need to be changed if the bar feature is enabled. Argh! you say. Back to the drawing board! Not so—​you can save all the duplication with a simple macro that, given an item and appropriate attributes, dumps them out with the documentation attributes added. Here’s a simple example:

macro_rules! define_traitor {
    (#[$m:meta] $t:item) => {
        /// All the doc comment lines and
        #[cfg_attr(feature = "bar", doc = "cfg_attr lines and")]
        /// so forth.
        #[$m] $t
    }
}

define_traitor! {
    #[cfg(not(feature = "bar"))]
    trait Traitor { }
}

define_traitor! {
    #[cfg(feature = "bar")]
    trait Traitor: BarOfChocolate { }
    // Yeah, a traitor can depend on a bar of chocolate, why not?
}

VoilĂ ! Whichever version of the Traitor trait we get, it has the right documentation on it.