Constant, compile-time `default` implementations in Rust

Published on Sep 28, 2021 | Updated on Sep 29, 2021

You can find the source code here↗, the crate here↗ and the docs here↗.

The problem

You must have come across the Default trait several times while writing code in Rust, and it’s a great convenience for many. In case you haven’t, it lets us get a default value for a given type. This can be extremely useful in cases where, say if a value is not provided – you can default to a value appropriate for that type.

For example, say you have a CliArgs struct that holds the configuration parsed from command line arguments. It may look like this:

struct CliArgs {
    superfast_mode: Option<u8>,
    // ... some other fields ...
}

Let’s assume that our hypothetical superfast mode that could have been there in our CLI arguments will make our program use some magic to run faster, and a higher number will cause the program to run even faster. Now, in the call site where we want to actually run the operation we would want to check what superfast_mode was set, and run accordingly. To do this, we might do something like:

fn run(args: CliArgs) -> std::io::Result<()> {
    let mode = args.superfast_mode.unwrap_or_default();
    if mode == 0 {
        // run with default mode
        run_default()
    } else {
        // run with magic
        run_superfast_with_mode(mode)
    }
}

The above is a trivial example, but you can see that it is very convenient to have a simple way to get the default value for a type. However, there’s one caveat – due to the limitations imposed by RFC 911, we can’t use the Default trait in constant contexts currently. Oops, that’s a bummer.

Say, for example you wanted a mutable static like this:

#[derive(Default)]
struct CurrentLoad {
    diskload: usize,
    netload: usize,
    portload: [usize; 65535],
}

static mut GLOBAL_LOAD: CurrentLoad = CurrentLoad::default();

// And you wanted to do this
fn handle_new_disk_process(...) -> ... {
    unsafe {
        GLOBAL_LOAD.diskload += 1;
    }
    // ...
}

fn handle_new_port_process(port: u16, ...) -> ... {
    unsafe {
        GLOBAL_LOAD.portload[port as usize] += 1;
    }
}

If you try to compile this, rustc will complain with the error:

error[E0015]: calls in constants are limited to tuple structs and tuple variants

We expected that, didn’t we? But can we get around this….with some magic? Well, let’s see!

The solution

The solution at this point in time is a macro that can “look into” the types and provide the correct values at compile-time (for use in const contexts). Well, that’s exactly what the derived::Constdef↗ macro does. To solve the above situation, this is all we need to do:

use derived::Constdef;

#[derive(Constdef)]
struct CurrentLoad {
    diskload: usize,
    netload: usize,
    portload: [usize; 65535],
}

// now declare your static
static mut GLOBAL_LOAD: CurrentLoad = CurrentLoad::default();
// and do what you want! ...
fn handle_new_port_process(port: u16, ...) -> ... {
    unsafe {
        GLOBAL_LOAD.portload[port as usize] += 1;
    }
}

Wait, … that’s all? Yeah! But what magic did the Constdef macro do?

The (not so) magical part

Behind the scenes, the Constdef peeks into the AST to look at the fields inside your struct. It then checks if the fields can be initialized to default values in a constant context. If they can, then Constdef will substitute the requisite values in the impl. To achieve calling in constant contexts, the macro will:

  • Add a pub const fn default() -> Self { ... } associated method to your struct
  • Add a Default trait implementation to your struct so that you can use it with things like Option::unwrap_or_default or mem::take

Update (September 29, 2021)

The macro now:

  • fully supports paths to primitives (std::primitives::u8 or core::primitives::u8 for example)
  • fully supports nesting:
    • Nested arrays
    • Nested tuples
    • Nesting arrays in tuples
    • Nesting tuples in arrays

Read more in this post.

What’s next?

This section is outdated. Read above

Currently, the Constdef supports using a limited subset of types↗ in constant contexts. The next thing that is to be worked on is supporting nested arrays, followed by supporting tuples. If you have any other ideas, drop them here↗ and let’s see what we can do to make them constable!