/ Rust

What I Like About Rust: No Uninitialized Variables

In one of Jonathan Blow's videos on Jai he discusses the issue of leaving variables uninitialized versus giving them default values. C and C++ leave variables uninitialized by default, saving the cost of having to write to memory if the default value will be immediately overwritten anyway; Higher level languages like Java and C# always initialize variables with default values, avoiding a myriad of potential issues that could result from the variable going uninitialized. In designing a language this is a common question that comes down to safety vs performance: It's safer to give all variables default values, but it's faster to not. Hearing Blow discuss how he deals with this in Jai made me appreciate the fact that Rust avoids this issue entirely.

In (Safe) Rust you can never have an uninitialized variable, but at the same time Rust will not assign a default value to a variable or struct member; You must always provide an initial value. This is possible in Rust, unlike in most C-like language, because Rust treats almost all syntactic blocks as expressions (meaning they evaluate to a value).

For example, say you have a variable that you want to conditionally initialize:

float myNum;
if ( someBool )
{
    myNum = 12.3456f;
}
else
{
    myNum = 986.23f;
}

This is a common situation in C or C++: You have some variable you want to define, but you want to give it a value conditionally. The pattern shown above of leaving the variable uninitialized and assigning the value within a conditional block is common in C and C++, but becomes potentially unsafe in more complex scenarios:

enum CoolEnum
{
    VARIANT_A,
    VARIANT_B,
    VARIANT_C,
    VARIANT_D,
}

float myNum;
switch ( someEnum )
{
case VARIANT_A:
    myNum = 12.34f;
    break;
case VARIANT_B:
    myNum = 987.6f;
    break;
case VARIANT_C:
    myNum = -65.0f;
    break;
// Uh-Oh! VARIANT_D leaves myNum uninitialized!
}

In the case above a simple mistake on the part of the programmer leaves myNum uninitialized some of the time, which can potentially cause hard to diagnose bugs down the line.

Here are the two examples in Rust:

let my_num = if some_bool {
    12.3456
} else {
    986.23
};

And

enum CoolEnum
{
    VariantA,
    VariantB,
    VariantC,
    VariantD,
}

float myNum = match someEnum {
    CoolEnum::VariantA => 12.34,
    CoolEnum::VariantB => 987.6,
    CoolEnum::VariantC => -65.0f,
    // ERROR! Not all match cases covered (missing VariantD), will not compile.
    // _ => 0.0, // Even if you include a default case it must have the same return type.
};

The secret sauce in both of them is that if blocks and match blocks are both expressions. An expression is anything that evaluates to a values, so in C 5+5 and foo.bar() are expressions, but if (foo) { foo->bar(); } is not. In rust almost all curly-brace wrapped blocks are expressions that evaluate to the value of their last line (the exception being loops, which always evaluate to (), which is like void in Rust). As you can see from the examples above this allows you to perform more complex calculations, even branching ones, when initializing a variable without paying the cost of having to initialize it with a temporary value or take the risk of leaving it uninitialized.

That said, there are cases where you would like to leave a variable truly uninitialized. This is of course Unsafe because many types (like bool, &T, and *T) have specific meanings and can't be filled with arbitrary data. In my experience this has only been helpful with calling into C system functions; Many system calls will expect a pointer to a struct so that they can fill in configuration data, so initializing the struct yourself isn't valuable and there's no realy loss of safety because C FFI is already unsafe. For these cases std::mem::uninitialized() comes in handy.

David LeGare

David is a game developer and systems programmer. He likes cats, he tolerates dogs, and he often remembers to dress himself before leaving the house.

Read More