Size: a a a

2019 December 05
Блог*
#meme #моё
источник
Блог*
#prog #rust
Допустим, ты пишешь на 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 {
       fn requires_zero_size(self) {
           const _: () = Self::I_AM_ZERO_SIZED;
           println!("requires_zero_size called");
       }
   }
Теперь мы натыкаемся на ошибку E0401. Окей, если константа не сработает, может, просто возьмём значение?
    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 {
   fn requires_zero_size(self) {
       println!("requires_zero_size was called, but Self is not zero-sized, bwa-ha-ha-ha!");
   }
}
Конечно, мы могли бы написать blanket impl, но тогда пользователи трейта не смогли бы (по крайней мере, без специализации) использовать собственную реализацию Foo. (Есть ещё и третья проблема: даже без кастомного impl-a этот код каким-то образом компилируется на nightly, но с этим я точно ничего сделать не могу).
⬇️⬇️⬇️
источник
Блог*
⬆️⬆️⬆️
Окей, посмотрим на то, что static_assertions по настоящему делает в макросе assert_eq_size!:
#[macro_export]
macro_rules! assert_eq_size {
   ($x:ty, $($xs:ty),+ $(,)?) => {
       const _: fn() = || {
           $(let _ = $crate::_core::mem::transmute::<$x, $xs>;)+
       };
   };
}
Умный трюк, ничего не скажешь: std::mem::transmute проверяет, что типы имеют одинаковый размер, и отключить это поведение никак нельзя. К сожалению, попытка использовать этот макрос (как и его код) непосредственно  приведёт к уже знакомой нам ошибке E0401, но можем адаптировать этот подход для своего трейта:
pub mod zero_sized {
   pub trait ZeroSized: Sized {
       #[deny(const_err)]
       const I_AM_ZERO_SIZED: unsafe extern "rust-intrinsic" fn(Self);
   }

   impl<T: Sized> ZeroSized for T {
       const I_AM_ZERO_SIZED: unsafe extern "rust-intrinsic" fn(Self) = std::mem::transmute::<Self, ()>;
   }
}
Посмотрим, что скажет компилятор:
error[E0658]: intrinsics are subject to change
--> src/main.rs:4:32
 |
4 |         const I_AM_ZERO_SIZED: unsafe extern "rust-intrinsic" fn(Self);
 |                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0658]: intrinsics are subject to change
 --> src/main.rs:10:32
  |
10 |         const I_AM_ZERO_SIZED: unsafe extern "rust-intrinsic" fn(Self) = std::mem::transmute::<Self, ()>;
  |                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0308]: cannot coerce intrinsics to function pointers
 --> src/main.rs:10:74
  |
10 |         const I_AM_ZERO_SIZED: unsafe extern "rust-intrinsic" fn(Self) = std::mem::transmute::<Self, ()>;
  |                                                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot coerce intrinsics to function pointers
  |
  = note: expected type `unsafe extern "rust-intrinsic" fn(T)`
             found type `unsafe extern "rust-intrinsic" fn(T) {std::intrinsics::transmute::<T, ()>}`
...Ну, или не можем. Макросу достаточно просто получить значение, а нам нужен ещё и тип.

А если сделать нормальный тип?
pub mod zero_sized {
   pub trait ZeroSized: Sized {
       #[deny(const_err)]
       const I_AM_ZERO_SIZED: ();
   }

   impl<T: Sized> ZeroSized for T {
       const I_AM_ZERO_SIZED: () = (std::mem::transmute::<Self, ()>, ()).1;
   }
}
Что ж:
error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
 --> src/main.rs:10:38
  |
10 |         const I_AM_ZERO_SIZED: () = (std::mem::transmute::<Self, ()>, ()).1;
  |                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: source type: `T` (this type does not have a fixed size)
  = note: target type: `()` (0 bits)
Теперь мы получаем ошибку компиляции... Всегда. Даже без impl-ов.
⬇️⬇️⬇️
источник
Блог*
⬆️⬆️⬆️
А что нам, собственно говоря, нужно? Нам нужно доказательство того, что тип имеет нулевой размер. Доказательство чего-либо можно передать:
1) ограничением на тип (см. Send и Sync);
2) фактом успешного вычисления на этапе компиляции константы (см. static_assert_size);
3) значением специального типа.
Действительно, если тип имеет приватные поля, то единственный способ сконструировать значение этого типа (за вычетом unsafe, разумеется) — это использовать публичную функцию, которая конструирует значение этого типа. Что ж, давайте так и поступим:
pub mod zero_sized {
   use std::marker::PhantomData;
   
   // Доказательство должно быть параметризовано типом,
   // чтобы доказательство для одного типа не могло быть использовано
   // как доказательство другого типа.
   pub struct ZeroSizeProof<T>(PhantomData<T>, ());
   
   pub trait ZeroSized: Sized {
       #[deny(const_err)]
       const I_AM_ZERO_SIZED: ();
       fn proof_zero_size(&self) -> ZeroSizeProof<Self>;
   }

   // опять-таки, blanket impl во избежание переопределения
   impl<T: Sized> ZeroSized for T {
       const I_AM_ZERO_SIZED: ()  = [()][std::mem::size_of::<Self>()];
       fn proof_zero_size(&self) -> ZeroSizeProof<Self> {
           ZeroSizeProof(PhantomData, Self::I_AM_ZERO_SIZED)
       }
   }
}
И теперь будем явно требовать доказательство:
pub mod foo {
   use crate::zero_sized::*;

   pub trait Foo: ZeroSized {
       fn requires_zero_size(self, _proof: ZeroSizedProof<Self>) {
           println!("requires_zero_size called");
       }
   }
}
Оно работает! Для удобства, чтобы пользователю не надо было каждый раз создавать доказательство руками, можно дописать extension trait:
pub mod foo {
   use crate::zero_sized::*;

   pub trait Foo: ZeroSized {
       fn requires_zero_size(self, _proof: ZeroSizeProof<Self>) {
           println!("requires_zero_size called");
       }
   }
   
   pub trait FooExt: Foo {
       fn requires_zero_size(self);
   }
   
   impl<T: Foo> FooExt for T {
       fn requires_zero_size(self) {
           let proof = self.proof_zero_size();
           <Self as Foo>::requires_zero_size(self, proof)
       }
   }
}
Проверим, как это работает:
use crate::zero_sized::ZeroSizeProof;

impl foo::Foo for () {}
impl foo::Foo for u32 {
   fn requires_zero_size(self, _proof: ZeroSizeProof<Self>) {
       println!("requires_zero_size was called, but Self is not zero-sized, bwa-ha-ha-ha!");
   }
}

fn main() {
   use foo::FooExt;
   ().requires_zero_size();
   42_u32.requires_zero_size();
}
Не компилируется! Как мы и хотели.
⬇️⬇️⬇️
источник
Блог*
⬆️⬆️⬆️
Что ж, теперь мы можем по праву гордиться тем, что изящно решили сложную проблему, не так ли?
⬇️⬇️⬇️
источник
Блог*
источник
Блог*
⬆️⬆️⬆️
На самом деле, это ужасно оверинжинирнутое решение.
Пользователь реализует один трейт, а в API используется другой.
Пользователь имеет дело с непонятным типом ZeroSizedProof.
Для системного языка, которым является Rust, это выглядит слишком сложным решением для простой задачи.
Но самое главное — наша желанная ошибка компиляции совершенно неинформативна:
error: any use of this value will cause an error
 --> src/main.rs:13:38
  |
13 |         const I_AM_ZERO_SIZED: ()  = [()][std::mem::size_of::<Self>()];
  |         -----------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-
  |                                      |
  |                                      index out of bounds: the len is 1 but the index is 4
  |
  = note: `#[deny(const_err)]` on by default
Как видите, никакого указания на то, где именно ошибка на самом деле.

Да, это решение работает. Но оно непрактично. Если не ненавидите всё человечество, просто вставьте у себя в нужном месте assert_eq!(std::mem::size_of::<Self>(), 0); и задокументируете это, компилятор это вырежет как мёртвый код.

Я всё.
источник
2019 December 06
Блог*
#meme
источник
Блог*
источник
Блог*
dereference_pointer_there
#prog #rust
Допустим, ты пишешь на 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 {
       fn requires_zero_size(self) {
           const _: () = Self::I_AM_ZERO_SIZED;
           println!("requires_zero_size called");
       }
   }
Теперь мы натыкаемся на ошибку E0401. Окей, если константа не сработает, может, просто возьмём значение?
    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 {
   fn requires_zero_size(self) {
       println!("requires_zero_size was called, but Self is not zero-sized, bwa-ha-ha-ha!");
   }
}
Конечно, мы могли бы написать blanket impl, но тогда пользователи трейта не смогли бы (по крайней мере, без специализации) использовать собственную реализацию Foo. (Есть ещё и третья проблема: даже без кастомного impl-a этот код каким-то образом компилируется на nightly, но с этим я точно ничего сделать не могу).
⬇️⬇️⬇️
Компилируется на nightly, когда не должно? Это ошибка. Кстати, на бете почему-то тоже компилируется.
источник
Блог*
#meme
источник
Блог*
Когда нанял диванного воина, а он оказался настоящим.
источник
2019 December 07
Блог*
источник
Блог*
#meme
источник
Блог*
источник
Блог*
В каждой шутке есть доля стартапа
источник
Блог*
Мы, конечно, знаем, что программисты умнее обычных людей, но это не заставляет их автоматически правильно говорить на английском.
За наводку спасибо @lilfunctor.
youtube.com/watch?v=jXJFuqHnF2Q
источник
2019 December 08
Блог*
Монументальная задача. Удачи.
https://github.com/sapir/gcc-rust/tree/rust
источник
Блог*
#meme
источник
2019 December 09
Блог*
источник