在编译时限制参数值

Limit parameter values at compile time

提问人:xjn xjn 提问时间:10/19/2023 最后编辑:cafce25xjn xjn 更新时间:10/19/2023 访问量:78

问:

Rust 中有没有办法根据泛型类型将参数值限制为范围或条件?

给定为无符号,或Tu8u16

const fn do_something_with_a_bit_offset<T>(offset: u8) -> T {
    // .. some ops return a T ..
}

如果是,偏移量可以是0..7,偏移量>7不应该编译
如果是,偏移量可以是0..15,偏移量>15不应该编译
Tu8Tu16

    let mask = do_something_with_a_bit_offset::<u8>(0);  // OK
    let mask = do_something_with_a_bit_offset::<u8>(8);  // Compile error
    let mask = do_something_with_a_bit_offset::<u16>(0);  // OK
    let mask = do_something_with_a_bit_offset::<u16>(8);  // OK
    let mask = do_something_with_a_bit_offset::<u16>(16);  // Compile error

如何在编译时执行此操作?

rust 编译时

答:

1赞 EvilTak 10/19/2023 #1

在开始之前,我必须提到一个显而易见的事实:如果不是编译时常量,则无法避免运行时检查。offset

如果参数保证为编译时常量,则可以使用 const 泛型和一些相关的常量魔术:offset

trait CheckOffset<const OFFSET: usize> {
    const CHECKED_OFFSET: usize;
}

impl<T: MaxOffset, const OFFSET: usize> CheckOffset<OFFSET> for T {
    const CHECKED_OFFSET: usize = if OFFSET <= T::MAX_OFFSET {
        OFFSET
    } else {
        panic!("Invalid offset")    // Compile time panic
    };
}

CheckOffset是一个帮助程序特征,它定义了一个关联的常量,该常量(在其实现中)实际上执行偏移量检查。仅当给定的偏移量小于 () 的最大偏移量时,才会编译对 的引用。 是一个帮助程序特征,用于定义类型的最大位偏移量:CHECKED_OFFSET<T as CheckOffset<OFFSET>>::CHECKED_OFFSETOFFSETT<T as MaxOffset>::MAX_OFFSETMaxOffset

trait MaxOffset {
    const MAX_OFFSET: usize;
}

impl<T> MaxOffset for T {
    const MAX_OFFSET: usize = 8 * size_of::<T>() - 1;
}

如果您需要一个函数来获取给定类型的编译时检查偏移量,则必须T

  1. 添加一个 const 泛型参数 () 来表示输入偏移量OFFSET
  2. 向类型添加约束CheckOffset<OFFSET>T
  3. 用作实际偏移量。如果不使用常量,则不会计算关联常量实现中的最大偏移量检查。<T as CheckOffset<OFFSET>>::CHECKED_OFFSETCHECKED_OFFSET
fn foo<T: CheckOffset<OFFSET> + 'static, const OFFSET: usize>() {
    let offset = T::CHECKED_OFFSET;
    println!("{} Offset: {:?}", type_name::<T>(), offset);
}

fn main() {
    foo::<u8, 0>();
    foo::<u8, 7>();
    // foo::<u8, 8>();      // Compile error

    foo::<u16, 0>();
    foo::<u16, 15>();
    // foo::<u16, 16>();    // Compile error

    foo::<u32, 0>();
    foo::<u32, 31>();
    // foo::<u32, 32>();    // Compile error
}

此方法适用于您可能具有的任何编译时常量可计算约束。

操场

0赞 Chayim Friedman 10/19/2023 #2

如果是运行时常量,则无法在所有情况下避免检查,但可以将验证它的责任从函数解除到调用方。这样做的优点是:失败是清楚地传达的(通过等),当使用文字时,你可以在编译时验证它。offsetunwrap()

这个想法是创建一个表示“范围内值”的结构:

mod private {
    pub trait Sealed {}
    impl Sealed for u8 {}
    impl Sealed for u16 {}
}

use std::ops::RangeInclusive;
use std::marker::PhantomData;

pub trait RangeLimited: private::Sealed {
    const ALLOWED_RANGE: RangeInclusive<u8>;
}

impl RangeLimited for u8 {
    const ALLOWED_RANGE: RangeInclusive<u8> = 0..=7;
}

impl RangeLimited for u16 {
    const ALLOWED_RANGE: RangeInclusive<u8> = 0..=15;
}

#[derive(Debug, Clone, Copy)]
pub struct InRange<T> {
    value: u8,
    _marker: PhantomData<T>,
}

impl<T: RangeLimited> InRange<T> {
    pub fn new(value: u8) -> Option<Self> {
        T::ALLOWED_RANGE.contains(&value).then_some(Self {
            value,
            _marker: PhantomData,
        })
    }

    pub fn get(self) -> u8 {
        self.value
    }
}

const fn do_something_with_a_bit_offset<T>(offset: InRange<T>) -> T {
    // .. some ops return a T ..
}

用法如下:

let mask = do_something_with_a_bit_offset::<u8>(InRange::new(0).unwrap());

当使用文字(或任何常量值)时,编译器可以在编译时对其进行验证,并为您节省一个 .为此,我们需要一个宏:unwrap()

impl<T: RangeLimited> InRange<T> {
    #[doc(hidden)]
    pub const fn new_unchecked(value: u8) -> Self {
        Self {
            value,
            _marker: PhantomData,
        }
    }
}

#[macro_export]
macro_rules! in_range {
    ($type:ty, $v:expr) => {{
        const __IN_RANGE_VALUE: $crate::InRange<$type> = {
            const __IN_RANGE_VALUE: $type = $v;
            if !(*<$type as $crate::RangeLimited>::ALLOWED_RANGE.start() <= __IN_RANGE_VALUE
                && __IN_RANGE_VALUE <= *<$type as $crate::RangeLimited>::ALLOWED_RANGE.start())
            {
                panic!("value out of range");
            }
            $crate::InRange::new_unchecked(__IN_RANGE_VALUE)
        };
        __IN_RANGE_VALUE
    }};
}

用法如下:

let mask = do_something_with_a_bit_offset(in_range!(u8, 0));

这是强制编译器在编译时评估它,否则它将在运行时评估它,这将是运行时崩溃。const

如果你想依靠值在范围内来获得健全性,你需要使 unsafe 并在宏中围绕它添加一个块(但是,要小心不要也包装在同一个不安全的块中)。new_unchecked()unsafe$v