编译时检查?
有没有更好的方法来做到这一点,或者这个运行时检查是最好的 Rust 1.0 支持的吗?
一般来说,有一些 hacky 解决方案可以对任意条件进行某种编译时测试。例如,the static_assertions crate 提供了一些有用的宏(包括一个类似于 C++ 的static_assert 的宏)。但是,这是hacky 并且非常有限。
在您的特定情况下,我还没有找到在编译时执行检查的方法。这里的根本问题是您不能在泛型类型上使用mem::size_of 或mem::transmute。相关问题:#43408 和 #47966。因此,static_assertions crate 也不起作用。
如果你仔细想想,这也会导致 Rust 程序员非常不熟悉的一种错误:实例化具有特定类型的泛型函数时的错误。这对于 C++ 程序员来说是众所周知的——Rust 的 trait bound 用于修复那些通常非常糟糕和无用的错误消息。在 Rust 世界中,需要将您的要求指定为 trait bound:类似于where size_of::<T> == size_of::<usize>()。
但是,目前这是不可能的。曾经有一个相当有名的"const-dependent type system" RFC 允许这种界限,但现在被拒绝了。对这些类型的功能的支持正在缓慢而稳步地进行。 “Miri” 不久前被合并到编译器中,允许更强大的常量评估。这是许多事情的推动者,包括the "Const Generics" RFC,它实际上已被合并。暂未实施,但预计2018年或2019年落地。
不幸的是,它仍然无法启用您需要的那种绑定。比较两个 const 表达式是否相等,was purposefully left out of the main RFC 将在未来的 RFC 中解决。
因此可以预期类似于where size_of::<T> == size_of::<usize>() 的绑定最终将成为可能。但这不应该在不久的将来发生!
解决方法
在你的情况下,我可能会引入一个 unsafe trait AsBigAsUsize。要实现它,您可以编写一个宏 impl_as_big_as_usize 来执行大小检查并实现特征。也许是这样的:
unsafe trait AsBigAsUsize: Sized {
const _DUMMY: [(); 0];
}
macro_rules! impl_as_big_as_usize {
($type:ty) => {
unsafe impl AsBigAsUsize for $type {
const _DUMMY: [(); 0] =
[(); (mem::size_of::<$type>() == mem::size_of::<usize>()) as usize];
// We should probably also check the alignment!
}
}
}
这使用了与static_assertions 使用的基本相同的技巧。这是可行的,因为我们从不在泛型类型上使用size_of,而只在宏调用的具体类型上使用。
所以...这显然远非完美。您的库的用户必须为他们想要在您的数据结构中使用的每种类型调用一次impl_as_big_as_usize。但至少它是安全的:只要程序员只使用宏来实现 trait,实际上 trait 只针对与 usize 大小相同的类型实现。还有,“trait bound AsBigAsUsize is not满足”这个错误也很好理解。
运行时检查呢?
正如 bluss 在 cmets 中所说,在您的 assert! 代码中,没有运行时检查,因为优化器会不断折叠检查。让我们用这段代码测试一下这个语句:
#![feature(asm)]
fn main() {
foo(3u64);
foo(true);
}
#[inline(never)]
fn foo<T>(t: T) {
use std::mem::size_of;
unsafe { asm!("" : : "r"(&t)) }; // black box
assert!(size_of::<usize>() == size_of::<T>());
unsafe { asm!("" : : "r"(&t)) }; // black box
}
疯狂的asm!() 表达式有两个目的:
- 从 LLVM “隐藏”
t,这样 LLVM 就无法执行我们不想要的优化(比如删除整个函数)
- 在我们将要查看的 ASM 代码中标记特定点
用夜间编译器编译它(在 64 位环境中!):
rustc -O --emit=asm test.rs
像往常一样,生成的汇编代码很难阅读;这里是重要的地方(有一些清理):
_ZN4test4main17he67e990f1745b02cE: # main()
subq $40, %rsp
callq _ZN4test3foo17hc593d7aa7187abe3E
callq _ZN4test3foo17h40b6a7d0419c9482E
ud2
_ZN4test3foo17h40b6a7d0419c9482E: # foo<bool>()
subq $40, %rsp
movb $1, 39(%rsp)
leaq 39(%rsp), %rax
#APP
#NO_APP
callq _ZN3std9panicking11begin_panic17h0914615a412ba184E
ud2
_ZN4test3foo17hc593d7aa7187abe3E: # foo<u64>()
pushq %rax
movq $3, (%rsp)
leaq (%rsp), %rax
#APP
#NO_APP
#APP
#NO_APP
popq %rax
retq
#APP-#NO_APP 对是我们的asm!() 表达式。
-
foo<bool> 案例:您可以看到我们的第一条asm!() 指令已编译,然后对panic!() 进行无条件调用,然后什么也没有(ud2 只是说“程序永远无法到达这个位置, panic!() 发散”)。
-
foo<u64> 案例:您可以看到两个 #APP-#NO_APP 对(两个 asm!() 表达式),中间没有任何内容。
所以是的:编译器完全删除检查。
如果编译器拒绝编译代码会更好。但这种方式我们至少知道,没有运行时开销。