LINUX.ORG.RU

Rust. Overflow checks на этапе компиляции.

 ,


0

5

Привет, ЛОР. Начал изучать Rust, за плечами 10 лет разработки на Java/Kotlin, так что в теме не зеленый, но вопрос возник очень примитивный.

Смотрим книгу по типам данных, там написано следующее.

Let’s say you have a variable of type u8 that can hold values between 0 and 255. If you try to change the variable to a value outside that range, such as 256, integer overflow will occur, which can result in one of two behaviors. When you’re compiling in debug mode, Rust includes checks for integer overflow that cause your program to panic at runtime if this behavior occurs. Rust uses the term panicking when a program exits with an error; we’ll discuss panics in more depth in the “Unrecoverable Errors with panic!” section in Chapter 9.

When you’re compiling in release mode with the --release flag, Rust does not include checks for integer overflow that cause panics. Instead, if overflow occurs, Rust performs two’s complement wrapping. In short, values greater than the maximum value the type can hold “wrap around” to the minimum of the values the type can hold. In the case of a u8, the value 256 becomes 0, the value 257 becomes 1, and so on. The program won’t panic, but the variable will have a value that probably isn’t what you were expecting it to have. Relying on integer overflow’s wrapping behavior is considered an error.

Читая прямо и в лоб, если собираем в debug - упадем в panic, если в релизе - получим 0/1/2, в зависимости от того, насколько вышли за границы u8.

А теперь практика.

fn main() {
    let mut number: u8 = 255;
    number += 1;
}

Вот этот код не собирается, ни в debug, ни в release, ошибка компилятора следующая

error: this arithmetic operation will overflow
 --> src/main.rs:3:5
  |
3 |     number += 1;
  |     ^^^^^^^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

Однако, стоить добавить всего одну строчку к этому коду

fn main() {
    let mut number: u8 = 255;
    number += 1;
    println!("The number is: {number}");
}

И поведение программы соответствует описанному в книге, собственно, вопрос: Почему вызов println! так влияет на это? В первом случае оно даже не собирается и overflow ловится на этапе компиляции, но стоит добавить println! и все начинает работать совсем иначе.

★★★★
Ответ на: комментарий от dataman

Добавление вызова println! игнорирует проверку и падает в runtime, в то время как без этого вызова (первый пример) overflow отлавливается еще на этапе компиляции.

Jefail ★★★★
() автор топика
Ответ на: комментарий от Jefail

Добавление вызова println! игнорирует проверку и падает в runtime

Не игнорирует. Просто это макрос, и обработка ошибки передаётся ему.
Ну, я так думаю. Я сварщик ненастоящий. :)

dataman ★★★★★
()
Ответ на: комментарий от dataman

Это очень странно со стороны диванного разработчика, потому что сам overflow явно виден компилятору еще до того, как он в принципе вызовет этот макрос. Т.е. логически нет никакого смысла до этого вызова даже доходить, поскольку и так понятно, что ничего хорошего не получится.

Jefail ★★★★
() автор топика

Наверное, какие-то приколы оптимизатора.

https://users.rust-lang.org/t/rust-arithmetic-overflow/85999/18

println раскрывается во взятие адреса, и видимо этого становится достаточно, чтобы перестать анализировать происходящее как compile-time constant expression.

Если нужно гарантировать compile-time вычисления, к твоим услугам const { ... }. В противном случае «как пойдёт» — наличие или отсутствие каких-то оптимизаций языком не гарантируется, они best-effort.

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 2)
Ответ на: комментарий от intelfx

Огромное спасибо за пояснение. Действительно, если сделать вот так

fn main() {
    let number = const {
        let mut number: u8 = 255;
        number + 1
    };
    println!("The value of number is: {number}");
}

То падать будет на этапе компиляции, вне зависимости от наличия или отсутствия println!

Jefail ★★★★
() автор топика
Ответ на: комментарий от Jefail

похоже, тут даже и не в оптимизации дело, а в мутабельности другая функция или действие могут «отменить» этот оверфлоу и в программе в целом его не будет вот это в релизе даже отработает правильно

    let mut number: u8 = 255;
    number += 10;
    number -=20;
    println!("The number is: {number}");

а гарантировать отлов оверфлоу можно просто убрав мутабельность let без mut

zurg
()
Ответ на: комментарий от zurg

Overflow в релизе в любом случае работает, просто если выйти за пределы - мы получим превышение, мутабельность не объясняет, почему в первом случае оно ловится, а с макросом нет. В любом случае, вариант ответа intelfx больше всего похож на правду, const дает гарантию, что все рассчеты производятся в compile-time, в то время как без него - на усмотрение и без гарантий (и в будущем это поведение может плавать).

Jefail ★★★★
() автор топика