Чему равно выражение -3/3u*3 на С++? Не угадаете. Ответ: -4. Приглашаю на небольшое расследование

Всё гораздо серьёзнее. Вот пример для проверки:

#include <iostream>

int main()
{
    std::cout << "-3/3u*3 = " << int(-3/3u*3) << "\n";
}

Посмотреть результат можно тут.

Или попробуйте поиграться с этим примером здесь или здесь.

Вообще-то мне не удалось найти хоть какой-то компилятор С++, который бы выдавал результат отличный от -4. Даже старый GCC-4.1.2, clang-3.0.0 или Borland C 1992 года. Также заметил, что результат одинаковый и для константы, вычисляемой в момент компиляции и для времени выполнения.

Предлагаю внимательно рассмотреть результат выражения -3/3u*3.

Если убрать приведение к типу intв примере выше, то получим 4294967292 или 0xFFFFFFFС(-4). Получается, что компилятор на самом деле считает результат беззнаковым и равным 4294967292. До этого момента я был свято уверен, что если в выражении используется знаковый тип, то и результат будет знаковым. Логично же это.

Если посмотреть откуда берется -4 вместо -3, посмотрим внимательней на ассемблерный код примера, например здесь.

Пример изменю, чтобы результат вычислялся не в момент компиляции, а в момент выполнения:

int main()
{
    volatile unsigned B = 3;
    int A = -3/B*3;
}

Для x86-64 clang 12.0.0 видим, что используется беззнаковое деление, хотя числитель откровенно отрицательное -3:

        mov     dword ptr [rbp - 4], 3    // B = 3
        mov     ecx, dword ptr [rbp - 4]
        mov     eax, 4294967293
        xor     edx, edx
        div     ecx                       // беззнаковое деление !!
        imul    eax, eax, 3               // знаковое умножение
        mov     dword ptr [rbp - 8], eax

Для x64 msvc v19.28 тот же подход к делению:

        mov     DWORD PTR B$[rsp], 3      // B = 3
        mov     eax, DWORD PTR B$[rsp]
        mov     DWORD PTR tv64[rsp], eax
        xor     edx, edx
        mov     eax, -3                             ; fffffffdH
        mov     ecx, DWORD PTR tv64[rsp]
        div     ecx
        imul    eax, eax, 3
        mov     DWORD PTR A$[rsp], eax

Получается, что для деления беззнакового числа на знаковое используется БЕЗЗНАКОВАЯ операция деления процессора div. Кстати, следующая команда процессора, это правильное знаковое умножение imul. Ну явный баг компилятора. Банальная логика же подсказывает, что знаковый тип выиграет в приведении типа результата выражения если оба знаковый и беззнаковый типы используются в выражении. И для знакового деления требуется знаковая команда деления процессора idiv, чтоб получить правильный результат со знаком.

Проблема еще и в том, что число 4294967293 не делится на 3 без остатка: 4294967293 = 1431655764 * 3 + 1 и при умножении 1431655764 обратно на 3, получаем 4294967292 или -4. Так что прикинуться веником и считать, что 4294967293 это то же -3, только вид сбоку, для операции деления не прокатит.

Двоично-дополнительное придставление отрицательных чисел.

Благодаря представлению чисел в двоично-дополнительном виде, операции сложения или вычитания над знаковыми и беззнаковыми числами выполняются одной и той же командой процессора (add для сложения и sub для вычитания). Процессор складывает (или вычитает) только знаковое со знаковым или только беззнаковое с беззнаковым. И для обоих этих операций используется одна команда add (или sub) и побитово результат будет одинаковый (если бы кто-то решил сделать раздельные операции сложения для знаковых и беззнаковых типов). Различие только во флагах процессора. Так что считать знаковое беззнаковым и складывать их оба как беззнаковых корректно и результат будет побитово правильным в обоих случаях. Но для деления и умножения этот подход в корне неправильный. Процессор внутри использует только беззнаковые числа для деления и умножения и результат приводит обратно в знаковое с правильным признаком знака. И для этого процессор использует разные команды для знакового (idiv) и беззнакового деления (div) и так же и для умножения (imul и соответственно mul).

Я когда обнаружил, что используется беззнаковое деление, решил, что это бага компилятора. Протестировал много компиляторов: msvc, gcc, clang. Все показали такой же результат, даже древние трудяги. Но мне довольно быстро подсказали, что это поведение описано и закреплено в самом стандарте.

Действительно, стандарт говорит об этом прямо:

Otherwise, if the unsigned operand's conversion rank is greater or equal to the conversion rank of" "the signed operand, the signed operand is converted to the unsigned operand's type.

Иначе, если ранг преобразования беззнакового операнда больше или равен рангу преобразования знакового операнда, то знаковый операнд приводится к типу беззнакового операнда.

Вот где оказывается собака зарыта: "the signed operand is converted to the unsigned operand's type"!! Ну почему, почему, Карл!! Логичнее наоборот: "the unsigned operand is converted to the signed operand's type", разумеется при соблюдении ранга преобразования. Ну вот как -3 представить беззнаковым числом?? Наоборот кстати можно.

Интересная получается сегрегация по знаковому признаку!

Заметил, что это правило почему-то работает только для операции деления, а операция умножения вычисляется правильно.

Проверим на ассемблере здесь этот пример:

int main()
{
    volatile unsigned B = 3;
    int C = -3*B;
}
Вот ассемблерный код:

mov dword ptr [rbp - 4], 3 mov eax, dword ptr [rbp - 4] imul eax, eax, 4294967293 mov dword ptr [rbp - 8], eax

Стандарт ничего не говорит о неприменимости этого правила для операции умножения. И деление и умножение должны быть БЕЗЗНАКОВЫМИ.

Я подумал, что что-то в стандарте сломалось и надо срочно это исправить. Написал письмо напрямую в поддержку Стандарта со всеми моими выкладками и предложением поправить это странное правило стандарта пока еще не поздно.

Ага! Наивный!

Мне ответил любезный молодой сотрудник из Стандарта и подтвердил, что мои выкладки правильны, логика на моей стороне, но поведение компиляторов полностью согласуется со Стандартом и само правило менять не будут, так как оно такое древнее, что непонятно кто и когда его ввел (сотрудник сказал, что искал автора, но не нашел) и поэтому его, как святую корову, трогать никто не будет, хотя вроде логично было бы исправить. Ну и милостиво разрешил поведать эту историю миру. О чем и пишу.

Хоть это исследование и было больше года назад, я до сих пор под впечатлением от многих вещей в этой истории:

  • Как я не натыкался на это раньше? Не один десяток лет интенсивно кодирую на С и С++ с погружением в ассемблер, но только сейчас споткнулся на неё. Хотя может и натыкался ранее, но не мог поверить что причина именно в этом.

  • Ассемблер я выучил раньше С и поэтому всегда считал С удобной заменой Ассемблера. Но правила работы процессоров неизменны и даже языки высокого уровня им следуют.

Интересно, что это правило всё же не работает и для операций с указателями. С ними нет такого поведения. Видать понимают разработчики компиляторов, что иначе рухнет очень многое. Посмотрите этот пример здесь:

int main()
{
    const unsigned a[] = {3,4,5,6,7};
    unsigned p = (&a[0] - &a[3])/3u*3;    // -3
    unsigned b = -3/3u*3;   // -4
}

Хоть я и понимаю, что могу ошибаться в логике работы этого мира, но задумайтесь, в следующий раз садясь в современный, нашпигованный вычислительной логикой самолёт (или автомобиль), а не сработает ли вдруг не оттестированный кусок кода в какой-то редкой нештатной ситуации, и не выдаст ли он -4 вместо элементарных -3, и не постигнет ли его участь подобная Boeing 737 MAX?

Учитывая, что сейчас кода на С++ производится гораздо больше, чем его успевают покрывать тестами, надежда, что будет отловлена такая базовая операция, тает ниже плинтуса.

Ошибка в команде просессора FDIV у Интела

Помните, в начале 2000-х была выявлена ошибка с вычислением в команде FDIV у Интела. Там было различие в 5 знаке после запятой в какой-то операции деления. Какой был шум тогда!!
Но исправили оперативно и быстро. В компиляторы добавили условный флаг для обхода этой команды. Интел срочно исправил логику в кристалле и выпустил новые чипы.

И это всего лишь 5-й знак после запятой! Многие его даже и не заметили, подумаешь, мелочь какая! А тут -4 вместо -3 и считаем знаковое беззнаковым и вместо -3 имеем еще и 4294967292! И тишина в ответ! И в этой тишине тихо падают Боинги.

Предвижу возражение, что есть рекомендация использовать преимущественно беззнаковый тип. Понимаю, с таким правилом Стандарта по другому никак. Но как -4 в беззнаковое перевести, и чтоб всё остальное работало? И почему на других языках и всех процессорах не надо этими танцами с бубном заниматься, а в С++ надо? И что делать с константными выражениями, вычисляемыми компилятором? Вместо элегантных выражений, в которых мы уверены, надо записывать их с оглядкой, чтоб у компилятора небыло изжоги!

Так же найдутся, кто возразит, что ничего нельзя менять, поздно уже, много кода написано, и если в нём есть костыли, то он рухнет. Новые ошибки, они потом будут, а старые - они святое и уже превратились в знакомые фичи.

Как хорошую подсказку познавательно добавить предупреждение в компилятор когда он применяет это правило из Стандарта: "Signed value is intentionally converted to unsigned value. Sorry for crashing one more airplane. Have a nice flight!" Вот удивимся тогда, как мало мы тестируем и как много нам открытий чудных приносит компилятор друг.

Можно еще исправить Стандарт, ограничив правило только операциями сложения и вычитания. Как компромис. Но это крайне маловероятно в этой Вселенной. Да и Боингов еще много летает.

Представьте студента (С) на экзамене у преподавателя по информатике (П) в ВУЗе.

- П: Хорошо, последний вопрос на 5. Можно ли привести знаковое число к беззнаковому типу?
- С: Хе, можно. НУЖНО! Обязательно НУЖНО! Ставте 5, я пойду.
- П: Как НУЖНО?? О_О. Подумайте. Как можно представить, например, -4 беззнаковым числом? - С: Чего тут думать! Стандарт С++ сказал, что НУЖНО, значит НУЖНО и точка. А то что -4 станет беззнаковым очень большим числом - это уже ни печалька Стандарта, ни моя. - П: Подумайте еще раз. Вы на экзамене по информатике и вас спрашивают о базовых вещах, которые общие для всех языков программирования и процессоров, а не только для С++. - С: Чего вы пристали к мне со своими языками и процессорами. У меня в билете вопрос про С++, вот я про С++ и отвечаю. А вы про какой то там Ассемблер, базовые вещи, языки программирования! Стандарт С++ сказал, компилятор сделал, я ответил! У вас есть вопросы к Стандарту про базовые вещи, вот ему их и задавайте, а я ответил правильно! - П: Да уж. Подстава конкретная.

Комментарии 86

    +31
    Если специально ломать код, то его можно поломать.

    Если не быть изобретательным программистом, то такой код просто не компилируется:
    Error C4308 negative integral constant converted to unsigned type
      +11
      Интересно, что это правило всё же не работает и для операций с указателями. С ними нет такого поведения.

      Результат зависит от разрядности процессора.
      При компиляции под 32 bit результатом будет –4, а под 64 bit — –3.
      Догадаетесь, почему?


      Для тех, кто не понял

      Разность между указателями — ptrdiff_t, это знаковый тип.
      Далее: у нас арифметическая операция между ptrdiff_t и uint32_t.
      В случае 32-битной архитектуры: ptrdiff_t — это int32_t, преобразуется в беззнаковый, результатом будет uint32_t (беззнаковый тип).
      В случае 64-битной архитектуры: ptrdiff_t — это int64_t, второй аргумент преобразуется в знаковый int64_t, результатом будет int64_t (знаковый тип).

        +2
        Да, вы правы. Значит всё ещё хуже на самом деле, чем даже я ожидал.
        +3

        Смешивать типы плохо, нужно явно приводить к одному, не просто бездумно, а именно к тому, что хочешь получить. Кастить -4 к беззнаковому в общем случае полная ерунда, поэтому если кастишь, то должен быть точно уверен зачем.
        А смешивать типы… ээ Просто не надо так…
        Это из серии сдвига 1 влево.

          +15

          Просто C++ — язык со слабой типизацией, и об этом надо помнить.
          А в C#, кстати, подобное поведение невозможно: там int32 op uint32 будет приведено к int64.

            +1
            Представьте, что вы написали выражение, в которое вовлечены одни константы, представленные по разному, constexpr, define, const auto и так далее. Не вами они написаны, но вы их используете. И используете его в разных местах кода, которые требуют разные типы char, short, int, long, int64_t, int128_t, и -3 запросто может быть приведена к этим типам. Вот ответьте: Вы будете вводить 6 одинаковых констант с разными требуемыми типами, или введете одну, но будете доверять компилятору, что он сам выведет правильный тип и будет использовать правильные операции приведения? Без танцев с явным приведением типов.
              +2

              Именно по причине небезопасности подобных штук появились <numeric_limits>, например. Заниматься такой арифметикой при наличии такого зоопарка надо либо с -Werror -Wpedantic, либо при помощи математических библиотек с конвертируемыми юнитами.


              Без танцев с явным приведением типов.

              Почему без, когда это именно то что помогает избегать проблем? narrowing, signed-unsigned comparison/operations, type overflow и прочее и прочее.

                +4

                Стоит отметить, что -Wpedantic (а также -Wall и -Wextra) нужного предупреждения не содержит, нужно отдельно включать -Wsign-conversion.

                  +1
                  MSVC с параметрами по умолчанию не компилирует код из примера, выдавая ошибку. Даже без уровня предупреждений 4, неговоря уже про Wall.
                    0

                    Я что-то не так делаю?
                    https://godbolt.org/z/s4GaKPEPE

                      0
                      Да, вы же х64 собираете, а ошибка только в х86 так себя ведёт.
                        +1

                        Поменял на x86, результат тот же. Почему оно вообще должно зависеть от архитектуры? У x64 MSVC int 64-битный?

                          0
                          в х64 это выражение преобразовывается в ptrdiff_t и отрабатывает правильно… Я вообще на живой студии запускал, как этот сайт работает я не знаю.
                            0
                            Сразу хочу усомниться в «полезности» этого онайн-компилятора, раз результат компиляции отличается от реальной студии. Для тестирования перенсоимости кода это сайт явно не походит.
                              0
                              www.onlinegdb.com/online_c++_compiler
                              www.programiz.com/cpp-programming/online-compiler
                              cpp.sh

                              взял из выдачи гугла первые три онлайн-компилятора, они тоже "-4" возвращают.
                  +1
                  Кастить -4 к беззнаковому в общем случае полная ерунда, поэтому если кастишь, то должен быть точно уверен зачем.

                  Совершенно верно!
                  Тут как бы деление с остатком (внезапно это 1), т.е. чтобы из -4 сделать -3 (или 4294967293), нужно по всем правилам добавить тот остаток после умножения, т.е. написать такое:


                  // (4294967293 / 3 * 3 + 4294967293 % 3) == 4294967293 == -3 (as signed int) 
                  (-3/3u)*3 + (-3%3u)
                  // ну или кастить собственно до деления:
                  -3/(signed)3u*3

                  Я вообще не понимаю о чем тут баттхерт и почему вдруг стандарт надо переписать?! Потому что человек вдруг открыл для себя увидел usual arithmetic conversions в действии? Ну ОК тогда.

                  +3
                  В отличии от деления, где знаковость влияет на результат, imul и mul отличаются лишь тем, какие флаги процессора в каком случае они меняют, побитово их результаты совпадают, при этом imul гибче, поэтому и используется компиляторами.

                  То, что для умножения правила преобразования типов разной знаковости такие же, можно убедиться так:

                  #include <iostream>
                  
                  int main() {
                    int i = 3;
                    unsigned u = 3;
                    if (((i * u) / (-3)) == 0) {
                      std::cout << "unsigned mul" << std::endl;
                    } else {
                      std::cout << "signed mul" << std::endl;
                    }
                  }
                  
                    +2

                    Верно. Операция MUL жёстко привязана к конкретным регистрам (DX:AX), тогда как IMUL может применяться к любым регистрам (но при этом не происходит расширение разрядности).

                      +1
                      Старшая половина произведения различается: например, 0xFF MUL 0xFF == 0xFE01, 0xFF IMUL 0xFF == 0x0001.
                      Но на Си, где тип произведения совпадает с типом множителей, эта разница не влияет.
                        0
                        то есть на чистом Си будет правильный результат, а на С++ выдает ошибку?
                          +2
                          Нет, в этом аспекте они совпадают.
                          0

                          Так при использовании IMUL старшая половина просто отбрасывается.

                            0
                            У IMUL есть варианты и с отбрасыванием, и без отбрасывания.
                              +1

                              Есть, но:


                              1. В языке C++ операция умножения предполагает отбрасывание. При перемножении 32-битных чисел результат остаётся 32-битным, а при умножении 64-битных — 64-битным.


                              2. Они менее удобны в использовании и потому компиляторы ими не пользуются.
                                Если нужно перемножить 32-битное число на 64-битное, то компилятор просто преобразует 32-битный аргумент в 64-битный, а не трахается с пересылкой между регистрами.


                              3. Единственный сценарий, когда используется MUL/IMUL без отбрасывания — это когда при перемножении 64-битных чисел (стандартный тип с максимальной разрядностью) мы хотим получить 128-битное (разрядность больше максимальной). Но т.к. 128-битное целое не является стандартным типом и задействует интринсики, то под x64 использование MUL/IMUL без отбрасывания для стандартных типов не имеет сценариев для использования.



                              Также MUL/IMUL без отбрасывания используется при перемножении 64-битных чисел на 32-битной архитектуре. В этом случае в чистом виде эту инструкцию можно увидеть при uint32 × uint32 → uint64, например


                              unsigned long long mul(unsigned a, unsigned b)
                              {
                                  return (unsigned long long)a * b;
                              }
                        0
                        «the unsigned operand is converted to the signed operand's type», разумеется при соблюдении ранга преобразования

                        Это ничего принципиально не изменит. uint32_t просто будет преобразован в int32_t («при соблюдении ранга преобразования» же) и, скажем, 4294967292u в тех же ILP32/LP64 не начнет волшебным образом делиться нацело на -3 (хотя по наивному рассуждению «должно» — впрочем, оно и при текущих правилах преобразования нацело не делится, просто сейчас будет один остаток, а тогда другой). О, сколько нам открытий чудных готовит просвещения дух, в общем.
                          +1

                          Странно, на носу уже с++23 маячит (опять туда напихают непонятно чего), но ни у кого руки не дойдут старое лигаси из стандарта вычистить… Слепое следование сломанному стандарту именно в таких случаях значительно хуже, чем исправление проблемы на уровне компилятора.

                            +5

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

                              0

                              Просто этот фикс надо явно внести как новое правило в новый стандарт, которое отменяет старое. Тогда всё становится очевидно и однозначно.
                              Но, честно говоря, если у кого-то код работал "правильно" именно благодаря этому багу, то тут уже код править надо прежде всего.

                                +2
                                Я не уверен, что хотя бы 10% программистов читает стандарты, и уж тем более новые стандарты.
                                  0
                                  Этот топик как раз и подтверждает, что при неожиданном поведении компилятора программист смотрит в стандарт языка в последнюю очередь — хотя поговорка «If all else fails, read the instructions» намного старше компьютеров.
                                0
                                Если, как в Rust, тупо запретить арифметические действия над несовпадающими типами, и пока не добавят явные касты в каждую строчку, код не скомпилируется — то ещё больнее.
                                  +1
                                  С этим есть теоретическая проблема. Она еще в языке Ада встречалась. Если под числами подразумеваются физические величины( а так часто случается), то метр * метр дают квадратный метр, а не метр. Или вектор * скаляр = вектор. Или 16 bit * 16 bit =~ 32 бита И тд. А Ваше утверждение равносильно тому, что операция умножения (и деления) всегда определена только строго внутри одного типа.
                                    0
                                    Это не ко мне, а к авторам Rust
                                      0
                                      Извиняюсь, конечно не Вам, а строгой типизации в принципе.
                                    +2

                                    Ну да, сделать из языка со слабой типизацией язык с сильной типизацией.
                                    Это когда любое приведение типов (кроме расширяющего) будет являться ошибкой.
                                    Вот только это будет уже другой язык.

                                  +3
                                  Если правила C++ вам не нравятся — просто пишите на тех языках, чьи правила вам нравятся.
                                    +3

                                    Мне не нравится тупость человеческая, и тупое следование явно нелогичным правилам, вместо того, чтобы вносить необходимые корректировки.

                                      +4
                                      Хорошо, возьмём воображаемый «исправленный C++», в котором действие над int32_t и uint32_t считается знаковым.
                                      Получим 1000000000*3u/3 == -431655765, тогда как сейчас получаем 1000000000.
                                      В каком из двух случаев «тупое следование явно нелогичным правилам»?
                                        0
                                        А почему бы компилятору не выдавать предупреждения в таком случае?

                                        Причём во всех случаях неявных преобразований встроенных типов, а не только этом, в первую очередь совсем безумных вроде float->bool
                                          +8
                                          Так ведь выдаёт!

                                          979880163/source.cpp:7:38: warning: implicit conversion changes signedness: 'int' to 'unsigned int' [-Wsign-conversion]
                                              std::cout << "-3/3u*3 = " << int(-3/3u*3) << "\n";
                                                                               ^~~
                                          1 warning generated.
                                            +2
                                            gcc 10.2.0 выдает предупреждение только если добавить -Wsign-conversion.
                                            При этом -Wall -Wextra -Wpedantic не выдают ничего. (На -Wpedantic иронично конечно надеяться в данном случае)
                                              0
                                              Отлично. Теперь надо и для остальных случаев, типа float->bool, char* -> bool и прочих целых.

                                              И главное, предупреждения по умолчанию (!!!), только с помощью хитрых опций — отключать.

                                              В стандарты это включено? А то на одном компиляторе предупредит, а на другом — нет. Вот чем комитет стандартизации должен заниматься.
                                                0
                                                -Wconversion покрывает такие случаи.
                                            +1

                                            Не путайте грешное с праведным. Целочисленное переполнение появляется по причине 32-битной переменной, независимо от языка и стандарта.

                                              +2
                                              В существующем ныне C++ — не появляется.
                                          +2
                                          — Шеф! У нас убыток 3 ляма! Что делать?
                                          — Раскидай его на троих. Скажи бухгалтеру, пусть посчитает доли, он в ИТ шарит.
                                          Бухгалтер:
                                          #include <iostream>
                                          int main()
                                          {
                                              std::cout << -3/3u;
                                          }


                                          1431655764
                                          — Шеф! Ура! Вы в выигрыше! Каждый из учредителей, по расчетам убытков, заработал 1431655764 ляма! Мы СуперМультиБиллионеры! Слава С++! Да здравствует Стандарт!
                                        +2
                                        Заметил, что это правило почему-то работает только для операции деления, а операция умножения вычисляется правильно.

                                        Нет, конечно, а проверяете вы неправильно https://godbolt.org/z/Pfrjvvbx7. И для деления, и для умножения применяются usual arithmetic conversions.
                                        Я подумал, что что-то в стандарте сломалось и надо срочно это исправить. Написал письмо напрямую в поддержку Стандарта со всеми моими выкладками и предложением поправить это странное правило стандарта пока еще не поздно.

                                        А integral promotions вы поправить не просили? Или просто ещё не натыкались на то, что перемножение двух unsigned short может приводить к неопределённому поведению?
                                          –1
                                          Результат тоже интересный
                                          unsigned short x = 65535;
                                          cout << x*x;
                                            +2

                                            Можно результат для тех, у кого нет под рукой возможности воспользоваться онлайн-компилятором из комментария выше, если не сложно?

                                              0
                                              -131071
                                                +1
                                                Знаковое переполнение — UB, так что теоретически тут может быть все что угодно.
                                          +1
                                          сначала протестируйте -3/3*3, потом идите спокойно спать. Делить меньший тип большим не есть хорошо.
                                            0
                                            Да и в целом, работа с беззнаковыми сопряжена с некоторыми опасностями. Безобидный цикл
                                            for (size_t i = N; i >= 0; i--)
                                            может никогда не завершиться
                                              +1
                                              Я бы сказал, «гарантированно никогда не завершится».
                                                +3

                                                Не-не. Он завершится при физическом уничтожении той машины, на которой он крутится, например. :)

                                                  +2
                                                  В этом смысле — да :)
                                                  Я имел в виду, что там нет UB, и компилятор обязан превратить этот код в бесконечный цикл.
                                                    +2
                                                      0

                                                      вроде бы переполнение unsigned типов это implementation defined behaviour (но без overflow), что почти так же плохо как UB. Поправьте, пожалуйста, если я ошибаюсь

                                                        +1
                                                        §6.2.5: A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.
                                                          +1

                                                          хм, судя по этому посту операции над "малыми" unsigned типами все же могут приводить к UB, так как для них арифметические операции не определены


                                                          In particular, arithmetic operators do not accept types smaller than int as arguments
                                                  0
                                                  Вот и я о том же. Но нет. -3 считает беззнаковым и стреляем в ногу.
                                                    0

                                                    Да, эта та штука, которая меня нереально бесит в C++. Почему нельзя использовать ssize_t? Смешивание знаковых и беззнаковых аргументов, да ещё и с неявными преобразованиям, является источником ошибок. Например, в C# и Java индексация — это int, а int op uint — это long. А ulong в Java так вообще нет.


                                                    Но на то есть свои причины. Например, производительность и стандарты двоичного представления, которые существуют только для беззнаковых чисел.

                                                      0
                                                      Например, в C# и Java индексация — это int, а int op uint — это long. А ulong в Java так вообще нет.

                                                      Так в Java и uint нет. Как у вас long получился?
                                                        +1
                                                        Так в Java и uint нет. Как у вас long получился?

                                                        Да, про Java напутал. Я просто на C# пишу, а не на Java.
                                                        Просто в C# попытка умножить long на ulong — это ошибка компиляции.

                                                  +2

                                                  Если бы в заголовке увидел каст результата к int, то даже не зашёл бы. А так — удивился, т.к. не понял, откуда в результате знаковое.
                                                  Ан нет, просто написано некорректно для привлечения внимания.


                                                  По теме: полезно просматривать флаги предупреждений компилятора, в частности -Wsign-conversion

                                                    +1
                                                    Не знаю, как такое можно не заметить. Уже смутно помню, откуда оно взялось, но четверть века следую правилу «не смешивать знак/беззнак в выражениях, иначе огребёшь проблем». И да, это самая бесячая заноза в С.
                                                      +1

                                                      https://cppinsights.io/s/366d310e


                                                      int f(int num) { return -3/3u*3;}  -> 
                                                      int f(int num) {
                                                        return static_cast<int>((static_cast<unsigned int>(-3) / 3U) * 3);
                                                      }

                                                      QED.

                                                        0

                                                        Надеюсь, в будущем системы контроля за ядерными вооружениями будут все-таки написаны на раст. И самолеты туда же.

                                                          +1
                                                          Мне ответил любезный молодой сотрудник из Стандарта и подтвердил, что мои выкладки правильны, логика на моей стороне, но поведение компиляторов полностью согласуется со Стандартом и само правило менять не будут, так как оно такое древнее, что непонятно кто и когда его ввел (сотрудник сказал, что искал автора, но не нашел) и поэтому его, как святую корову, трогать никто не будет, хотя вроде логично было бы исправить. Ну и милостиво разрешил поведать эту историю миру. О чем и пишу.

                                                          Я не автор этого правила, но оно мне кажется совершенно логичным: если я пишу /3, значит я имею в виду знаковое деление; если я пишу /3u, значит я имею в виду беззнаковое деление.
                                                          Если, как предлагают «исправить C++», оба этих варианта будут обозначать знаковое деление — то как обозначить беззнаковое? Явным кастом делимого, как в Rust?
                                                            +2

                                                            3u обозначает знаковое число и не более того. К операции деления это отношения не имеет.
                                                            Логичным правилом в случае явно знакового деления было бы приведение обоих операндов к знаковому предоставлению и выдача сообщения об ошибке, если это не возможно из за, например, переполнения.

                                                              0
                                                              Если, несмотря на явно беззнаковый делитель, вы считаете этот случай «явно знаковым делением» — то как должно выглядеть «явно беззнаковое деление»?
                                                            +1

                                                            Andrey2008: есть в PVS Studio тесты, выявляющие нечто подобное?

                                                              +2
                                                              Обдумаем.
                                                                0

                                                                У gcc есть -Wsign-compare

                                                                  0
                                                                  Существующие компиляторы и так выдают предупреждение
                                                                    0

                                                                    Выше в комментах написали, что "gcc 10.2.0 выдает предупреждение только если добавить -Wsign-conversion", ну это такое себе...

                                                                  +1

                                                                  Пишите на Java ))
                                                                  Не кидайте помидорами, я — обычный юзер

                                                                    0
                                                                    не сработает ли вдруг не оттестированный кусок кода в какой-то редкой нештатной ситуации

                                                                    потому что код в первую очередь должен быть хорошо читаемым, тогда и нештатных ситуаций будет меньше.
                                                                    про тестирование это отдельная тема. в Sony один раз забыли, что год может быть високосным, и у них PSN целый день по всему миру лежал.

                                                                      +1
                                                                      Каждая история знакомства с приведением типов в C и C++ выглядит примерно так — ярко и с ненавистью. Выше уже отметили, что корректного способа это вычислить нет — только не компилировать, что вы можете получить с -Werror.
                                                                      Вкину ещё, чтобы пожар не гас:
                                                                      auto foo() {
                                                                          uint16_t a = 3;
                                                                          uint16_t b = 2;
                                                                          return b - a; // int -1
                                                                      }
                                                                      
                                                                      auto bar() {
                                                                          uint16_t a = 3;
                                                                          int16_t b = -3;
                                                                          return b/a; // int -1
                                                                      }

                                                                      Никаких переполнений и знаковых чисел. Потому что для всех типов меньше int арифметические операции сначала конвертят операнды в int. То есть для маленьких типов всё так, как хочет автор.
                                                                        0
                                                                        — Чему равно -3/3u*3?
                                                                        — Увольнению.
                                                                          0
                                                                          Спасибо за прекрасный пример проблемы возможности слабой типизации. Эту возможность зачастую выдают за достоинство ЯП: гибкость. И Википедия отмечает:
                                                                          Сильная, но не полиморфная система типов может затруднить решение многих алгоритмических задач, как это было отмечено в отношении языка Pascal

                                                                          Про Паскаль сказано:
                                                                          Вариантная запись позволяет рассматривать тип данных различным образом в зависимости от указанного варианта.


                                                                          Да, такой обход был в листингах из "developer documentation manuals Inside Macintosh". Ну так это в явном виде обход сильной типизации с добавлением нескольких строк кода. И только в нескольких редких случаях. Поэтому возражения сторонников слабой типизации не убеждают:

                                                                          Так же найдутся, кто возразит, что ничего нельзя менять, поздно уже, много кода написано, и если в нём есть костыли, то он рухнет. Новые ошибки, они потом будут, а старые — они святое и уже превратились в знакомые фичи.

                                                                          Как хорошую подсказку познавательно добавить предупреждение в компилятор когда он применяет это правило из Стандарта: «Signed value is intentionally converted to unsigned value. Sorry for crashing one more airplane. Have a nice flight!» Вот удивимся тогда, как мало мы тестируем и как много нам открытий чудных приносит компилятор друг.


                                                                          Слабая типизация не должна быть по умолчанию. Сделайте директиву, отключающую типизацию, где это необходимо, а потом включающую. И старый код, после простой доработки, будет совместим с новым стандартом. При этом м.б. найдны баги, и спасено несколько Боингов.
                                                                            +1
                                                                            Кому нужна слабая типизация по умолчанию, те пишут на одних языках; кому нужна строгая типизация по умолчанию, те пишут на других языках. Возможность выбора — это прекрасно.
                                                                              0
                                                                              Согласен, что «Возможность выбора — это прекрасно»! Однако у тех, кто нанят поддерживать старый софт, нет выбора. ИМХО надежная типизация должна стать международным стандартом.
                                                                            +1
                                                                            Логичнее наоборот: «the unsigned operand is converted to the signed operand's type», разумеется при соблюдении ранга преобразования.
                                                                            И чем логичнее, что 4294967295u/2 будет равно 0?
                                                                              +1
                                                                              Что-то над плюсами не так весело смеются как над js

                                                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                                              Самое читаемое