Is this a bug? Trying to get effect of static_assert

Now that you can panic in const blocks I tried this:

pub fn f<T>() {
    const _: () = {
        if std::mem::align_of::<T>() != 1 {
            panic!("std::mem::align_of::<T>() must be 1!");
        }
    };
}

But I get the confusing error:

   Compiling playground v0.0.1 (/playground)
error[E0401]: can't use generic parameters from outer function
 --> src/lib.rs:3:33
  |
1 | pub fn f<T>() {
  |          - type parameter from outer function
2 |     const _: () = {
3 |         if std::mem::align_of::<T>() != 1 {
  |                                 ^ use of generic parameter from outer function

For more information about this error, try `rustc --explain E0401`.
error: could not compile `playground` due to previous error

Playground link

It's intentional, a const in a function is supposed to behave like a global const.

Does this suit your needs though?

3 Likes

The only downside is a trait based solution means expressing the bound everywhere in generic code. But that seems like what Rust probably wants me to do to avoid C++ style compiler error traces.

It's not a "downside", it's also intentional. Your const-based solution would cause a post-monomorphization error, ie. it wouldn't be guaranteed that once the body of the function is typechecked, it will compile for every suitable instantiation. That has proven to be way more confusing than useful, especially in library code, when one can't reasonably expect and test every foreseeable correct use, since there is an open set of types one may be able to instantiate generics with.

Having to put trait bounds on your types when you require specific capabilities explicitly indicates what you need from a type, like a contract. This upfront checking ensures precise and good error messages at the call site, which is much easier to debug.

I understand the general aversion to post monomorphization errors, and the Rust solution works but it's 15x the length of the C++ one and gives a less readable error message. It will also require copy and pasting another bound at every layer. I'm fine with traits restricting what behavior is available but I'm not sure throwing up obstacles to additional static checks is a win. If I were lazier I might just say my function is already unsafe anyway and not bother.

What I would normally do is assert the alignment constraint when constructing the type that cares about it, and from there you just pass that value around like normal.

For example, imagine we have a helper trait which gives us the alignment and we can use for constraining things.

trait Aligned: Sized {
    const ALIGNMENT: usize;
}

impl<T: Sized> Aligned for T {
    const ALIGNMENT: usize = std::mem::align_of::<Self>();
}

From there, we can write our constructor.

#![feature(associated_const_equality)]

struct Foo<T>(T);

impl<T> Foo<T> {
    fn new(value: T) -> Self
    where
        T: Aligned<ALIGNMENT = 1>,
    {
        Foo(value)
    }
}

And try to use it.

fn do_something_with_foo<T>(foo: Foo<T>) {
    println!("Doing something with Foo!");
}

fn main() {
    let _ = Foo::new(42_u8);
    let _ = Foo::new(42_u32);
}

(playground)

Trying to construct a Foo with a u32 results in this error:

error[E0271]: type mismatch resolving `<u32 as Aligned>::ALIGNMENT == 1_usize`
  --> src/main.rs:28:13
   |
28 |     let _ = Foo::new(42_u32);
   |             ^^^^^^^^ expected `1_usize`, found `4_usize`
   |
   = note: expected type `1_usize`
              found type `4_usize`
note: required by a bound in `Foo::<T>::new`
  --> src/main.rs:16:20
   |
14 |     fn new(value: T) -> Self
   |        --- required by a bound in this
15 |     where
16 |         T: Aligned<ALIGNMENT = 1>,
   |                    ^^^^^^^^^^^^^ required by this bound in `Foo::<T>::new`

It's a bit awkward and more verbose because you can't use expressions and assertions with const generics aren't fully fleshed out just yet.