Why is Rust's assert_eq! implemented using a match?

Alright, let's remove the match.

    macro_rules! assert_eq_2 {
        ($left:expr, $right:expr) => ({
            if !($left == $right) {
                panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, $left, $right)
            }
        });
    }

Now, let's pick a completely random example...

fn really_complex_fn() -> i32 {
    // Hit the disk, send some network requests,
    // and mine some bitcoin, then...
    return 1;
}

assert_eq_2!(really_complex_fn(), 1);

This would expand to...

{
    if !(really_complex_fn() == 1) {
        panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, really_complex_fn(), 1)
    }
}

As you can see, we're calling the function twice. That's less than ideal, even more so if the result of the function could change each time it's called.

The match is just a quick, easy way to evaluate both "arguments" to the macro exactly once and bind them to variable names.


Using match ensures that the expressions $left and $right are each evaluated only once, and that any temporaries created during their evaluation live at least as long as the result bindings left and right.

An expansion which used $left and $right multiple times -- once while performing the comparison, and again when interpolating into an error message -- would behave unexpectedly if either expression had side effects. But why can't the expansion do something like let left = &$left; let right = &$right;?

Consider:

let vals = vec![1, 2, 3, 4].into_iter();
assert_eq!(vals.collect::<Vec<_>>().as_slice(), [1, 2, 3, 4]);

Suppose this expanded to:

let left = &vals.collect::<Vec<_>>().as_slice();
let right = &[1,2,3,4];
if !(*left == *right) {
    panic!("...");
}

In Rust, the lifetime of temporaries produced within a statement is generally limited to the statement itself. Therefore, this expansion is an error:

error[E0597]: borrowed value does not live long enough
  --> src/main.rs:5:21
   |
5  |         let left = &vals.collect::<Vec<_>>().as_slice();
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^           - temporary value dropped here while still borrowed
   |                     |
   |                     temporary value does not live long enough

The temporary vals.collect::<Vec<_>>() needs to live at least as long as left, but in fact it is dropped at the end of the let statement.

Contrast this with the expansion

match (&vals.collect::<Vec<_>>().as_slice(), &[1,2,3,4]) {
    (left, right) => {
        if !(*left == *right) {
            panic!("...");
        }
    }
}

This produces the same temporary, but its lifetime extends over the entire match expression -- long enough for us to compare left and right, and interpolate them into the error message if the comparison fails.

In this sense, match is Rust's let ... in construct.

Note that this situation is unchanged with non-lexical lifetimes. Despite its name, NLL does not change the lifetime of any values -- i.e. when they are dropped. It only makes the scopes of borrows more precise. So it does not help us in this situation.

Tags:

Rust