Why does this workaround to "can't use `Self` from outer item" work?

I would like to be able to call from within a method this compile-time assertion:

const _: () = assert!(Self::is_odd(123));

where is_odd is a const fn method.

This fails with can't use Self from outer item, which I can understand. Note that this is generated from within a macro so we do not have access to the qualified name of the struct and need to use Self.

My coworker found this workaround (inspired from the static_assert crate), which does work:

let _: [(); {
    if !Self::is_odd(123) {
        panic!("not odd!")
    };
    0
}] = [];

I don't really see why this is allowed and not the former. Is this a loophole that risks being closed one day, or can this be relied on?

Thanks for anyone able to enlighten me :slight_smile:

this is a let binding, which is a statement.

on the other hand, a const definition is an item, but in your case, it's not an associated item, thus it cannot use Self.

I do not fully understand what you mean by it not being an associated item: is_odd is a method of Self, doesn't that make it an associated item?

It also says (on rustc 1.85+):

a `const` is a separate item from the item that contains it

Which means that, all for intents and purposes, this:

struct Struct;

impl Struct {
    const fn is_odd(i: i32) -> bool { ... }
    pub fn some_method(&self) -> Self {
        // your const check
        const _: () = assert!(Self::is_odd(123));
        Struct
    }
}

and this:

struct Struct;

impl Struct {
    const fn is_odd(i: i32) -> bool { ... }
    pub fn some_method(&self) -> Self {
        Struct
    }
}

// what is `Self` supposed to be?
const _: () = assert!(Self::is_odd(123));

are equivalent. Also note:

impl Struct {
    const fn is_odd(i: i32) -> bool { false }
    // this will compile just fine, as associated consts are lazily evaluated
    const _ASSERT: () = assert!(Self::is_odd(0));
    // as will this, for the exact same reason
    const _BOMB: () = panic!("boom"); 
}
// but not this
const _BOMB: () = panic!("boom");

Thanks, yes, that makes sense.

But I still do not understand then why in the workaround Self::is_odd is evaluated a compile-time without problem (and rustc does actually require is_odd to be a const fn for this to work).

Here is a complete working example.

Because const-time expression and “const value” have different requirements.

Note that in today's Rust that workaround is no longer needed.
You can just write:

    const { assert!(Self::is_odd(123), "not odd"); }

And that works just fine. It's not limitation of constant evalution but limitation of a named const (that's still there even if name is omitted).

2 Likes

Thank you! This was what was bothering me...

And thanks for the nicer alternative too :slight_smile:

For posterity: array trick vs const block

The initial array-size workaround actually does have an advantage over the nicer-looking const { assert!(Self::is_odd(123), "not odd"); }: it seems to be evaluated earlier in the compilation process.

In our case, the assert is used to give a nicer error message before calling a (potentially) non-existing method. With the const block, the assert error is never shown and only the rather cryptic error about the missing method is. But presumably because the array type is resolved first, the initial workaround shows the assert error.

1 Like