
Size: a a a
as_str
методы к разным Split
-строковым итераторам недавно смерджил толян!const STRING: String = 42.to_string();Всё, всем спасибо за внимание, пост можно зака...
error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variantsА, ну да. Я совсем забыл, что Rust создан не для программирования — он создан для страданий. Ну, окей, перевод числа в строку на этапе компиляции — это невероятно часто используемая фича, наверняка в стандартной библиотеке есть что-то готовое. И, действительно, есть stringify!
--> src/lib.rs:1:24
|
1 | const STRING: String = 42.to_string();
| ^^^^^^^^^^^^^^
const STRING: &str = stringify!(42);
fn main() {
assert_eq!(STRING, "42");
}
Так, теперь точно можно заканчивать. Конечно, хардкодить константы нехорошо, так что дадим ей имя:const THE_ANSWER: usize = 42;
const STRING: &str = stringify!(THE_ANSWER);
fn main() {
assert_eq!(STRING, "42");
}
Да, всё работает, всем спасибо, до новых встре...thread 'main' panicked at 'assertion failed: `(left == right)`
left: `"THE_ANSWER"`,
right: `"42"`', src/main.rs:5:5
Погодите, что?stringify!
работает исключительно на синтаксическом уровне. Так что же, мы лишены нормального инструмента? Строго говоря, да... Но мы можем сделать свой инструмент!u32
:const fn to_ascii(mut n: u32) -> [u8; 20] {
let mut ret = [0; 20];
let mut i = 0;
while n != 0 {
ret[i] = (n % 10) as u8 + b'0';
n /= 10;
i += 1;
}
...
Окей, пока всё идёт неплохо — выделить цифру несложно, равно как и перевести её в символ ASCII. Но у нас строка получилась в обратном порядке. Окей, давайте обратим порядок — достаточно лишь вызвать reverse
: ...
ret[..i].reverse();
...
Теперь нам надо лишь...error[E0723]: mutable references in const fn are unstable
--> src/lib.rs:12:5
|
12 | ret[..i].reverse();
| ^^^^^^^^
|
= note: see issue #57563 <https://github.com/rust-lang/rust/issues/57563> for more information
= help: add `#![feature(const_fn)]` to the crate attributes to enable
Ну ёлки-палки, компилятор продолжает ставить палки в колёса. Выходит, надо писать переворот вручную. И задействовать <[u8]>::swap
мы тоже не можем, потому что эта функция, очевидно, также требует мутабельной ссылки (а ещё потому, что она не const fn
, но это уже не имеет значения). Стиснув зубы, пишем переворот в очень сишном стиле: ...
let len = i;
let mut i = 0; // да, я дважды использую одно и то же имя
// для разных счётчиков, останьте
while i < len / 2 {
let tmp = ret[i];
ret[i] = ret[len - i - 1];
ret[len - i - 1] = tmp;
i += 1;
}
...
warning: this looks like you are swapping elements of `ret` manually
--> src/lib.rs:14:9
|
14 | / let tmp = ret[i];
15 | | ret[i] = ret[len - i - 1];
16 | | ret[len - i - 1] = tmp;
| |______________________________^ help: try: `ret.swap(i, len - i - 1)`
|
= note: `#[warn(clippy::manual_swap)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_swap
Нет, Clippy, я не могу тут вызвать reverse
, я пытался! Когда ж ты поумнеешь? А пока что — вот тебе кляп: ...
#[allow(clippy::manual_swap)]
while i < len / 2 {
...
На чём я остановился? Ах да, мы возвращаем массив из функции и... Погодите, а как мы узнаем, где заканчивается строка? Сделать слайс не получится, потому что тогда мы будем возвращать ссылку на локальную переменную, а бровей чекер borrow checker бдит даже в const fn, зараза этакая. Выходит, нам надо возвращать вместе с массивом длину строки: ...
(ret, len)
}
Соответственно, нам надо также поменять и возвращаемый тип у функции:const fn to_ascii(mut n: u32) -> ([u8; 20], usize) {
...
Как нам теперь из этого добра получить &str
? Дело нехитрое: делаем слайс байтов, переводим в str — и дело в шляпе! Вернее, было бы в шляпе, если бы std::str::from_utf8
была бы const fn. А она таковой не является! Что же делать? Ладно, пока я над этим думаю, давайте проверим, что to_ascii
работает правильно — хрен с ними, с константами, будем сами в строку переделывать:fn main() {
for &x in &[42, 1, 0] {
let (bytes, len) = to_ascii(x);
println!("{}: {:?}", x, std::str::from_utf8(&bytes[..len]).unwrap());
}
}
Что оно выводит?42: "42"Ну, по крайней мере, оно хотя ра... Стоп, что?
1: "1"
0: ""
0: ""
Граничный случай! Окей, не вопрос, добавим if
в начале:...Теперь всё работает пра...
if n == 0 {
return (ret, 1);
}
...
42: "42"А, логично, нам нужен именно ASCII-шный символ нуля, а не нуль-символ:
1: "1"
0: "\u{0}"
...Вот теперь всё в порядке:
if n == 0 {
ret[0] = b'0';
return (ret, 1);
}
...
42: "42"Ладно, это всё прекрасно, но что нам делать с превращением всего этого в строку? Ну, раз честным путём пройти не вышло — считерим! В настоящий момент
1: "1"
0: "0"
&str
и &[u8]
имеют одно и то же представление, так что, хотя мы и не можем использовать str::str::from_utf8_unchecked
напрямую (это потребует nightly-фичи const_str_from_utf8_unchecked
), мы можем просто скопировать реализацию (кстати, по не вполне понятным мне причинам transmute
можно использовать внутри константных выражений, но не внутри const fn):const DECOMPOSED: ([u8; 20], usize) = to_ascii(42);Что ж, теперь мы можем спокойно распеча...
const STR: &str = unsafe { std::mem::transmute(&DECOMPOSED.0[..DECOMPOSED.1]) };
error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants...Облом. Опять. Что ж делать-то? Видимо, придётся читерить ещё больше. Если мы не можем сконструировать слайс, то мы можем сконструировать что-то, что имеет такое же представление, как и слайс, и потом универсальным превращателем
--> src/lib.rs:29:49
|
29 | const STR: &str = unsafe { std::mem::transmute(&DECOMPOSED.0[..DECOMPOSED.1]) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
transmute
преобразовать в строковой слайс. Но тут мы ступаем на шаткую территорию: хотя и известно, что слайс — это пара из указателя и длины, мы не знаем, в каком порядке они идут. Что ж, мы подсмотрим, какой порядок на самом деле и сделаем так же:#[repr(C)] // repr(C) обязателен, чтобы между полями не было паддинга
struct RawSlice {
ptr: *const u8,
len: usize
}
const DECOMPOSED: ([u8; 20], usize) = to_ascii(42);(В том, что мы берём ссылку от константы, нет ничего плохого из-за static promotion)
const STR: &str = unsafe { std::mem::transmute(RawSlice {
ptr: &DECOMPOSED.0 as *const _, // &[u8] неявно приводится к *const [u8],
// а тот уже кастуется в *const u8
len: DECOMPOSED.1,
})};
fn main() {
assert_eq!(STR, "42");
}
mod _sanity_check {Здесь мы создаём сырой указатель на слайс (и, насколько я понимаю, в данном случае вызов
use super::RawSlice;
use std::mem::transmute;
const _RAW_SLICE_HAS_RIGHT_REPR: [(); 1] = {
const SENTINEL: usize = 1342;
const SENTINEL_SLICE: RawSlice = RawSlice {
ptr: std::ptr::null(),
len: SENTINEL,
};
[(); (<*const [u8]>::len(unsafe { transmute(SENTINEL_SLICE) }) == SENTINEL) as _]
};
}
transmute
безопасен, поскольку у сырых толстых указателей значительно менее строгие требования к корректности, чем к ссылкам на слайс) с заданной длиной, а потом вынимаем её при помощи метода len сырого указателя. Если мы угадали с порядком полей, то сравнение длин возвратит true
, которое будет скастовано в 1usize
и, таким образом, образует литерал массива нужного типа. В данный момент код компилируется. Давайте поменяем порядок полей в RawSlice
:#[repr(C)]...и посмотрим, что скажет компилятор:
struct RawSlice {
len: usize,
ptr: *const u8,
}
...expected an array with a fixed size of 1 element, found one with 0 elements
Отлично, ровно то, что мы и хотели (на самом деле мы хотели бы более внятное сообщение об ошибке, но пока что имеем, что имеем).error: any use of this value will cause an errorЧто ж, ожидаемо. Но брать размер с запасом всё же не хочется. Мы не можем знать наперёд, сколько цифр понадобится... Хотя, погодите-ка, можем!
--> src/main.rs:27:9
|
27 | ret[i] = (n % 10) as u8 + b'0';
| ^^^^^^
| |
| index out of bounds: the length is 1 but the index is 1
| inside `to_ascii` at src/main.rs:27:9
| inside `DECOMPOSED` at src/main.rs:50:38
...
50 | const DECOMPOSED: ([u8; 1], usize) = to_ascii(42);
| --------------------------------------------------
|
= note: `#[deny(const_err)]` on by default
const fn digits_len(mut n: u32) -> usize {Отлично, это всё более-менее работает. Но это всё ещё ногострельно! Тут довольно много кода, который можно написать неправильно, а собственно число, для которого делается строка, приходится использовать дважды (да, я в курсе, что можно просто с запасом взять, нет, я не хочу так делать). Можем ли мы всё это как-то инкапсулировать? Можем! При помощи макроса:
if n == 0 {
return 1;
}
let mut n_digits = 0;
while n != 0 {
n /= 10;
n_digits += 1;
}
n_digits
}
const LEN: usize = digits_len(42);
const fn to_ascii(mut n: u32) -> ([u8; LEN], usize) {
...
macro_rules! make_literal {Проверим:
($n:expr) => {{
// ^первая пара скобок является частью синтаксиса macro_rules!,
// а вторая открывает блок
const VALUE: u32 = $n;
// далее весь код почти без изменений,
// только с заменой конкретного значения на VALUE
const STR: &str = ...;
STR
}}
}
const STR: &str = make_literal!(41 + 1);Оно работает!
fn main() {
assert_eq!(STR, "42");
}
u32
? Наш код вполне может работать с другими типами! Давайте это исправим:macro_rules! make_literal {
(($n:expr) : $ty:ty) => {{
// то же, что и было, но заменяем u32 на $ty
STR: &str = ...;
STR
}}
}
const STR: &str = make_literal!((41 + 1): u32);
fn main() {
assert_eq!(STR, "42");
}
const STR: &str = make_literal!((-41 - 1): i32);
fn main() {
assert_eq!(STR, "-42");
}
error: any use of this value will cause an error
--> src/main.rs:42:26
|
42 | ret[i] = (n % 10) as u8 + b'0';
| ^^^^^^^^^^^^^^^^^^^^^
| |
| attempt to compute `254_u8 + 48_u8`, which would overflow
| inside `to_ascii` at src/main.rs:42:26
| inside `DECOMPOSED` at src/main.rs:59:48
const fn extract_digit(n: $ty) -> $ty {
let mut ret = n % 10;
#[allow(unused_comparisons)]
// ^ сравнение не имеет смысла для беззнаковых чисел
if ret < 0 {
// мы не можем написать ret = -ret,
// поскольку унарный минус не определён для беззнаковых
ret = 0-ret;
}
ret
}
const fn digits_len(mut n: $ty) -> usize {
if n == 0 {
return 1;
}
let mut n_digits = 0;
#[allow(unused_comparisons)]
if n < 0 {
n_digits += 1;
}
...
}
...
const fn to_ascii(mut n: $ty) -> ([u8; LEN], usize) {
#[allow(unused_comparisons)]
let is_negative = n < 0;
...
while n != 0 {
ret[i] = extract_digit(n) as u8 + b'0';
n /= 10;
i += 1;
}
if is_negative {
ret[i] = b'-';
i += 1;
}
...
const STR: &str = make_literal!((-41 - 1): i32);
fn main() {
assert_eq!(STR, "-42");
}
macro_rules!
после expr
нельзя ставить двоеточие, из-за чего мне пришлось внести в синтаксис раздражающие скобки. Как известно, всякую проблему можно решить ещё одним слоем абстракции, поэтому я именно этим и воспользуюсь: я сделаю макрос, который принимает имя и тип и генерирует ещё один макрос с переданным именем, который принимает выражение нужного типа и уже возвращает константу:macro_rules! make_literal_maker {
($name:ident : $ty:ty) => {
macro_rules! $name {
($n:expr) => {{
// весь остальной код без изменений
}}
}
}
}
make_literal_maker!(make_str_literal_from_usize: usize);
make_literal_maker!(make_str_literal_from_i8: i8);
const STR_UNSIGNED: &str = make_str_literal_from_usize!(41 + 3 - 2);
const STR_SIGNED: &str = make_str_literal_from_i8!(-100 - 1);
fn main() {
assert_eq!(STR_UNSIGNED, "42");
assert_eq!(STR_SIGNED, "-101");
}