#prog #rust
Допустим, ты пишешь на Rust библиотеку и определяешь трейт, для вызова метода которого по каким-то причинам требуется, чтобы
Окей, наученный опытом static_assertions, ты пишешь примерно следующее:
То есть ошибка при вычислении константы трейта не возникает, если эту константу не использовать. В принципе, логично — зная определение трейта, компилятор не может наперёд сказать, будет ли константа вычислена для всех типов корректно. Что ж, поменяем определение
Значит ли это, что мы решили проблему? Ничего подобного. Во-первых, ошибка компиляции возникает при использовании метода, а не при определении impl-а. Во-вторых — и это куда как более серьёзная проблема — этот подход опирается на реализацию метода по умолчанию, которую можно переопределить:
⬇️⬇️⬇️
Допустим, ты пишешь на Rust библиотеку и определяешь трейт, для вызова метода которого по каким-то причинам требуется, чтобы
Self
был ZST. Для удобства дальнейшего изложения сделаем подобное определение:pub mod foo {В идеале для этого достаточно было бы навесить на
pub trait Foo {
fn requires_zero_size(self) {
println!("requires_zero_size called");
}
}
}
Self
ограничение : ZeroSized
, который является auto-трейтом, но... Такого трейта в std
нет.Окей, наученный опытом static_assertions, ты пишешь примерно следующее:
pub mod zero_sized {Проверим, как оно работает:
pub trait ZeroSized: Sized {
#[deny(const_err)] //потому что выше по скоупу может быть #[allow(const_err)]
const I_AM_ZERO_SIZED: ();
}
// blanket impl вместо дефолтного значения, чтобы I_AM_ZERO_SIZED нельзя было переопределить
impl<T: Sized> ZeroSized for T {
const I_AM_ZERO_SIZED: () = [()][std::mem::size_of::<Self>()]; //является ошибкой, если Self имеет ненулевой размер
}
}
pub mod foo {
pub trait Foo: super::zero_sized::ZeroSized {
fn requires_zero_size(self) {
println!("requires_zero_size called");
}
}
}
use foo::Foo;И оно... Компилируется.
impl Foo for () {}
impl Foo for u32 {}
То есть ошибка при вычислении константы трейта не возникает, если эту константу не использовать. В принципе, логично — зная определение трейта, компилятор не может наперёд сказать, будет ли константа вычислена для всех типов корректно. Что ж, поменяем определение
Foo
:pub trait Foo: super::zero_sized::ZeroSized {Теперь мы натыкаемся на ошибку E0401. Окей, если константа не сработает, может, просто возьмём значение?
fn requires_zero_size(self) {
const _: () = Self::I_AM_ZERO_SIZED;
println!("requires_zero_size called");
}
}
pub trait Foo: super::zero_sized::ZeroSized {И теперь... Ошибки компиляции нет. Что, давайте действительно вызовем этот метод:
fn requires_zero_size(self) {
let () = Self::I_AM_ZERO_SIZED;
println!("requires_zero_size called");
}
}
fn main() {Вот теперь ошибка компиляции есть.
().requires_zero_size();
42_u32.requires_zero_size();
}
Значит ли это, что мы решили проблему? Ничего подобного. Во-первых, ошибка компиляции возникает при использовании метода, а не при определении impl-а. Во-вторых — и это куда как более серьёзная проблема — этот подход опирается на реализацию метода по умолчанию, которую можно переопределить:
impl Foo for u32 {Конечно, мы могли бы написать blanket impl, но тогда пользователи трейта не смогли бы (по крайней мере, без специализации) использовать собственную реализацию Foo. (Есть ещё и третья проблема: даже без кастомного impl-a этот код каким-то образом компилируется на nightly, но с этим я точно ничего сделать не могу).
fn requires_zero_size(self) {
println!("requires_zero_size was called, but Self is not zero-sized, bwa-ha-ha-ha!");
}
}
⬇️⬇️⬇️
docs.rs
static_assertions - Rust
Banner