`if` 和 `match` 在 nightly Rust 的常量中可用

2019年11月25日 · Dylan MacKenzie 代表 WG const-eval

概览: ifmatch 现在可以在最新的 nightly 版本中的常量中使用。

因此,你现在可以编写如下代码,并在编译时执行:

static PLATFORM: &str = if cfg!(unix) {
    "unix"
} else if cfg!(windows) {
    "windows"
} else {
    "other"
};

const _: () = assert!(std::mem::size_of::<usize>() == 8, "Only 64-bit platforms are supported");

ifmatch 也可以在 const fn 的函数体中使用

const fn gcd(a: u32, b: u32) -> u32 {
    match (a, b) {
        (x, 0) | (0, x) => x,

        (x, y) if x % 2 == 0 && y % 2 == 0 => 2*gcd(x/2, y/2),
        (x, y) | (y, x) if x % 2 == 0 => gcd(x/2, y),

        (x, y) if x < y => gcd((y-x)/2, x),
        (x, y) => gcd((x-y)/2, y),
    }
}

这里到底发生了什么?

以下表达式,

  • match
  • ifif let
  • &&||

现在可以出现在以下任何上下文中,

  • const fn 函数体
  • const 和 associated const 初始化表达式
  • staticstatic mut 初始化表达式
  • 数组初始化表达式
  • const 泛型(实验性)

如果你的 crate 启用了 #![feature(const_if_match)]

你可能已经注意到,短路逻辑运算符 &&||conststatic 中已经是合法的了。这是通过将它们翻译成非短路等价形式 &| 来实现的。启用该 feature gate 将关闭这个 hack,并使 &&|| 按你预期的方式工作。

这些变化的一个副作用是,如果同时启用 #![feature(const_panic)],则 assertdebug_assert 宏可以在 const 上下文中使用。然而,其他 assert 宏(例如 assert_eqdebug_assert_ne)仍然被禁止,因为它们需要对其参数调用 Debug::fmt

循环构造 whileforloop 也被禁止,并将通过独立的 feature gate 启用。正如你在上面看到的,循环可以作为临时措施通过递归来模拟。然而,非递归版本通常会更有效率,因为 Rust(据我所知)不做尾调用优化。

最后,? 运算符在 const 上下文中仍然被禁止,因为它解糖后包含对 From::from 的调用。const trait 方法的设计仍在讨论中,而 ? 和解糖后调用 IntoIterator::into_iterfor,直到最终决定做出后才能使用。

下一步是什么?

这一改变将允许标准库中的大量函数变为 const。你可以帮助完成这个过程!这里有一份数字函数列表,可以轻松地进行 const 化。转换为 const fn 需要两个步骤。首先,在函数定义中添加 const#[rustc_const_unstable] 属性。这允许 nightly 用户在 const 上下文中调用它。然后,经过一段实验期后,移除该属性并稳定该函数的 const 属性。请参阅#61635 了解第一步的示例,参阅#64028 了解第二步的示例。

就我个人而言,我期待这个 feature 已经很久了,迫不及待地想开始试玩它。如果你也有同感,我将非常感谢你测试这个 feature 的极限!试着将 Cell 和带有 Drop 实现的类型潜入不应该出现的地方,用实现糟糕的递归函数(参见上面的 gcd)撑爆栈,如果出现严重问题,请告诉我们。

为什么花了这么长时间?

Miri 引擎是 Rust 内部用于编译时函数求值的工具,它已经具备这个能力一段时间了。然而,Rust 需要静态地保证 const 中变量的某些属性,例如它们是否允许内部可变性或是否具有需要调用的 Drop 实现。例如,我们必须拒绝以下代码,因为它会导致 const 在运行时可变!

const CELL: &std::cell::Cell<i32> = &std::cell::Cell::new(42); // Not allowed...

fn main() {
    CELL.set(0);
    println!("{}", CELL.get()); // otherwise this could print `0`!!!
}

然而,有时 const 包含对可能具有内部可变性的*类型*的引用是可以的,只要我们能证明该类型的实际*值*不具有。这对于带有“单元变体”(例如 Option::None)的 enum 尤其有用。

const NO_CELL: Option<&std::cell::Cell<i32>> = None; // OK

关于 const 上下文中 Drop 的规则内部可变性的规则 的更详细(但非规范性)的处理可以在 const-eval 仓库中找到。

当涉及复杂的控制流(如循环和条件语句)时,保证变量值的属性并非易事。实现此 feature 需要扩展 Rust 中现有的数据流框架,以便我们能够正确跟踪控制流图中的每个局部变量的值。目前,分析非常保守,尤其是在值移进和移出复合数据类型时。例如,即使启用了 feature gate,以下代码也不会编译:

const fn imprecise() -> Vec<i32> {
    let tuple: (Vec<i32>,) = (Vec::new(),);
    tuple.0
}

尽管 Vec::new 创建的 Vecconst fn 内部永远不会实际被 drop,但我们没有检测到 tuple 的所有字段都已移出,因此保守地假设 tuple 的 drop 实现将会运行。虽然这个特定情况很简单,但还有其他更复杂的情况需要更全面的解决方案。关于我们需要在此处有多精确是一个悬而未决的问题,因为更高的精度意味着更长的编译时间,即使对于不需要更多表达能力的用户也是如此。