Safe compile-time-checked NonZero_ constructors

#1

Just suggesting this off the top of my head. Currently building NonZero integers requires either unsafe construction or a runtime-checked (non const!) construction.

Both options are very disappointing when creating these NonZero values out of integer literals: either the input literal is 0, and compilation should fail, or it is not, and the unsafe conversion is valid.

So I was think of some proc-macro with a behavior along the following lines:

macro_rules! nonzero_u8 {
    (0) => (compile_error!("0 is not nonzero!"));

    (1) => (unsafe {
        ::std::num::NonZeroU8::new_unchecked(1)
    });
    (2) => (unsafe {
        ::std::num::NonZeroU8::new_unchecked(2)
    });
    (3) => (unsafe {
        ::std::num::NonZeroU8::new_unchecked(3)
    });
    // etc.
    (255) => (unsafe {
        ::std::num::NonZeroU8::new_unchecked(255)
    });
    ($other:tt) => (compile_error!("nonzero_u8! requires an integer literal in base 10"));
}

I have tested it while having the macro as an external crate, and when used with 0, we get the following error:

error: 0 is not nonzero!
 --> src/main.rs:6:24
  |
6 |     let x: NonZeroU8 = nonzero_u8!(0);
  |                        ^^^^^^^^^^^^^^
  |
  = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

With a proc_macro, obviously, we could get it to work even for NonZeroU64, and for any kind of literal syntax (e.g., 1_024_u32).

Now, the obvious answer is that this can be done in an external crate, and thus std is not required.

I think, however, that since it saves runtime cost or unsafe usage, this can definitely go to std (or even core!)

Obviously, that’s just my opinion, so I would like to have your thoughts on this:

  • I think avoiding unsafe makes it std-worthy
  • This should just be an external crate

0 voters

#2

Option 3 – wait until const expressions can support conditional code, then new can be const too.


8 Likes
#3

There is a (somewhat hacky) way to do this with a const fn safely,like this:

use std::num::NonZeroU8;

const fn new_non_zero_u8(value:u8)->NonZeroU8{
    ["value==0 should always be false"][(value==0)as usize];
    unsafe{
        NonZeroU8::new_unchecked(value)
    }
}


// The next line will error at compile-time if uncommented
//const N_0:NonZeroU8=new_non_zero_u8(0);

const N_1:NonZeroU8=new_non_zero_u8(1);

This has the advantage that it works with non-literal constants.

3 Likes
#4

You’re right, const fns are a cleaner way to get this, although it did not seem possible in stable yet (so I was about to make the external crate to have it in the meantime) until @rodrimati1992’s response: I am not versed in const fns, so I did not know about that const conditional syntax, but it works in stable and is exactly what I was looking for, while waiting for NonZeroU_::new to be const fn.

Note, the function still needs to be marked unsafe though, since it may be called in non const context, but at least when fed a literal we can have the peace of mind of that safety condition being checked at compile time.

EDIT: misread the function (I though it was an #[attr(..., ...)] magic const lang item :stuck_out_tongue:)

Thank you all for your input.

#5

Well it still surely panic on zero even on non-const context

#6

@Hyeonu you are right regarding safety, but since I am not fond of the panicking function (I’d rather use NonZeroU8::new() and explicitely .unwrap() afterwards for a non const expression), I suggest the following form, that prevents any runtime panic!

#[macro_export]
macro_rules! const_nonzero_u8 {(
    $value:expr
) => ({
    const RET: NonZeroU8 = {
        #[deny(const_err)] {
            let _const_guard: () = [()][($value == 0) as usize];
        }
        unsafe {
            NonZeroU8::new_unchecked($value)
        }
    };
    RET
})}

then, when feeding 0 from a downstream crate we get:

error[E0080]: erroneous constant used
 --> src/main.rs:6:24
  |
6 |     let x: NonZeroU8 = const_nonzero_u8!(0);
  |                        ^^^^^^^^^^^^^^^^^^^^ referenced constant has errors
  |
  = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

error: aborting due to previous error

For more information about this error, try `rustc --explain E0080`.

and when feeding a non constant expression we get:

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
 --> src/main.rs:6:42
  |
6 |     let x: NonZeroU8 = const_nonzero_u8!(::std::env::args().len() as u8);
  |                                          ^^^^^^^^^^^^^^^^^^

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
 --> src/main.rs:6:42
  |
6 |     let x: NonZeroU8 = const_nonzero_u8!(::std::env::args().len() as u8);
  |                                          ^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0015`.

or

error[E0434]: can't capture dynamic environment in a "fn" item
 --> src/main.rs:7:42
  |
7 |     let x: NonZeroU8 = const_nonzero_u8!(x);
  |                                          ^
  |
  = help: use the `|| { ... }` closure form instead

error: aborting due to previous error

For more information about this error, try `rustc --explain E0434`.
1 Like