RFC 2307: concrete-nonzero-types

libs (types-libstd)

Summary

Add std::num::NonZeroU32 and eleven other concrete types (one for each primitive integer type) to replace and deprecate core::nonzero::NonZero<T>. (Non-zero/non-null raw pointers are available through std::ptr::NonNull<U>.)

Background

The &T and &mut T types are represented in memory as pointers, and the type system ensures that they’re always valid. In particular, they can never be NULL. Since at least 2013, rustc has taken advantage of that fact to optimize the memory representation of Option<&T> and Option<&mut T> to be the same as &T and &mut T, with the forbidden NULL value indicating Option::None.

Later (still before Rust 1.0), a core::nonzero::NonZero<T> generic wrapper type was added to extend this optimization to raw pointers (as used in types like Box<T> or Vec<T>) and integers, encoding in the type system that they can not be null/zero. Its API today is:

#[lang = "non_zero"]
#[unstable]
pub struct NonZero<T: Zeroable>(T);

#[unstable]
impl<T: Zeroable> NonZero<T> {
    pub const unsafe fn new_unchecked(x: T) -> Self { NonZero(x) }
    pub fn new(x: T) -> Option<Self> { if x.is_zero() { None } else { Some(NonZero(x)) }}
    pub fn get(self) -> T { self.0 }
}

#[unstable]
pub unsafe trait Zeroable {
    fn is_zero(&self) -> bool;
}

impl Zeroable for /* {{i,u}{8, 16, 32, 64, 128, size}, *{const,mut} T where T: ?Sized} */

The tracking issue for these unstable APIs is rust#27730.

std::ptr::NonNull was stabilized in in Rust 1.25, wrapping NonZero further for raw pointers and adding pointer-specific APIs.

Motivation

With NonNull covering pointers, the remaining use cases for NonZero are integers.

One problem of the current API is that it is unclear what happens or what should happen to NonZero<T> or Option<NonZero<T>> when T is some type other than a raw pointer or a primitive integer. In particular, crates outside of std can implement Zeroable for their abitrary types since it is a public trait.

To avoid this question entirely, this RFC proposes replacing the generic type and trait with twelve concrete types in std::num, one for each primitive integer type. This is similar to the existing atomic integer types like std::sync::atomic::AtomicU32.

Guide-level explanation

When an integer value can never be zero because of the way an algorithm works, this fact can be encoded in the type system by using for example the NonZeroU32 type instead of u32.

This enables code recieving such a value to safely make some assuptions, for example that dividing by this value will not cause a attempt to divide by zero panic. This may also enable the compiler to make some memory optimizations, for example Option<NonZeroU32> might take no more space than u32 (with None represented as zero).

Reference-level explanation

A new private macro_rules! macro is defined and used in core::num that expands to twelve sets of items like below, one for each of:

These types are also re-exported in std::num.

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct NonZeroU32(NonZero<u32>);

impl NonZeroU32 {
    pub const unsafe fn new_unchecked(n: u32) -> Self { Self(NonZero(n)) }
    pub fn new(n: u32) -> Option<Self> { if n == 0 { None } else { Some(Self(NonZero(n))) }}
    pub fn get(self) -> u32 { self.0.0 }
}

impl fmt::Debug for NonZeroU32 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Debug::fmt(&self.get(), f)
    }
}

// Similar impls for Display, Binary, Octal, LowerHex, and UpperHex

Additionally, the core::nonzero module and its contents (NonZero and Zeroable) are deprecated with a warning message that suggests using ptr::NonNull or num::NonZero* instead.

A couple release cycles later, the module is made private to libcore and reduced to:

/// Implementation detail of `ptr::NonNull` and `num::NonZero*`
#[lang = "non_zero"]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) struct NonZero(pub(crate) T);

impl<T: CoerceUnsized<U>> CoerceUnsized<NonZero<U>> for NonZero<T> {}

The memory layout of Option<&T> is a documented guarantee of the Rust language. This RFC does not propose extending this guarantee to these new types. For example, size_of::<Option<NonZeroU32>>() == size_of::<NonZeroU32>() may or may not be true. It happens to be in current rustc, but an alternative Rust implementation could define num::NonZero* purely as library types.

Drawbacks

This adds to the ever-expanding API surface of the standard library.

Rationale and alternatives

Unresolved questions

Should the memory layout of e.g. Option<NonZeroU32> be a language guarantee?

Discussion of the design of a new language feature for integer types restricted to an arbitrary sub-range (see second unresolved question) is out of scope for this RFC. Discussing the potential existence of such a feature as a reason not to add non-zero integer types is in scope.