Чему равно выражение -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

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

UPD: В комментариях заметили, что IMUL богаче по вариантам использования, чем MUL. Да и результаты у них бывают одинаковые с отбрасыванием. Так что IMUL тут используется совсем не потому, что это правильно по логике.

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

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

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

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

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

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

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

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 станет беззнаковым очень большим числом - это уже ни печалька Стандарта, ни моя. - П: Подумайте еще раз. Вы на экзамене по информатике и вас спрашивают о базовых вещах, которые общие для всех языков программирования и процессоров, а не только для С++. - С: Чего вы пристали к мне со своими языками и процессорами. У меня в билете вопрос про С++, вот я про С++ и отвечаю. А вы про какой то там Ассемблер, базовые вещи, языки программирования! Стандарт С++ сказал, компилятор сделал, я ответил! У вас есть вопросы к Стандарту про базовые вещи, вот ему их и задавайте, а я ответил правильно! - П: Да уж. Подстава конкретная.

UPD: Провёл еще расследование. Спасибо комментаторам. Их как всегда читать очень интересно и познавательно. Для этого и опубликовано. Выражайте свое мнение!
Вот пример кода, где в операции деления участвуют числа a и -a в разных комбинациях и с декларацией явно unsigned. Результат ожидается быть -1. Из 15 примеров только 2 вернули -1, 3 вернули 4294967295, 6 - 0, остальные - разные числа.

результаты примера:

-1/1 = -1
1/-1 = -1
1u/-1 = 0
-1/1u = 4294967295
-1u/1 = 4294967295
1u/-1 = 0
2u/-2 = 0
3u/-3 = 0
4u/-4 = 0
5u/-5 = 0
-1/1u = 4294967295
-2/2u = 2147483647
-3/3u = 1431655764
-4/4u = 1073741823
-5/5u = 858993458

Вот еще пример кода, где знак - переносится к разным литералам и в результате имеем 3 разных ответа, хотя ответ не должен меняться по арифметической логике:

int main()
{
    constexpr auto s1 = -3/3u*3;   // -4
    constexpr auto s2 = 3u/-3*3;   // 0
    constexpr auto s3 = 3/3u*-3;   // -3
}
//test here: https://godbolt.org/

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

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

    Если не быть изобретательным программистом, то такой код просто не компилируется:
    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
        Да, вы правы. Значит всё ещё хуже на самом деле, чем даже я ожидал.
        +5

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

          +17

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

            –1

            А что же будет, если операция над Int64 и UInt64?


            В С++ конечно во все места разложили UB, но не то что бы в C# их нет. Например что будет в результате unchecked( Int32.MinValue / -1 )?

              +2
              А что же будет, если операция над Int64 и UInt64?

              Ошибка компиляции: будь добр, сначала сделай явное приведение типов.


              Например что будет в результате unchecked( Int32.MinValue / -1 )?

              Ну да, вы явно указали директивой unchecked свои намерения.

                0
                Ошибка компиляции: будь добр, сначала сделай явное приведение типов.

                Да, точно, здесь всё отлично.


                Ну да, вы явно указали директивой unchecked свои намерения

                Можно не указывать unchecked явно, в C# это поведение по умолчанию, если другое не указано в настройках проекта.


                На самом деле если убрать unchecked в такой записи, то просто не скомпилируется. Но если представить что -1 не константа на этапе компиляции, а результат некоторого выражения, то тут хоть явный unchecked, хоть не явный, будет UB.


                Например так:


                static int GetNum() => -1;
                int a = int.MinValue / GetNum();
                System.Console.WriteLine(a);
                  +2
                  Наверное, тут стоит быть чуть строже с терминами — оно не undefined, всё-таки, а
                  либо unspecified, либо implementation-defined.
                  И это катастрофическая разница — undefined, если я правильно помню, это «стандарт не говорит ничего о поведении программы в этом случае, в зависимости от случайных факторов может происходить всё что угодно». Unspecified — точно не описано, но может быть диапазон вариантов, и для одного кода и окружения результат воспроизводим, implementation-defined — описано, но в документации компилятора/интерпретатора, а не языка.
                    +1

                    Всё так, это implementation-defined.


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


                    И хотя implementation-defined это не тоже самое, но не вижу причин, почему он не может привести к подобным последствиям. Например почему разработчик компилятора не может сказать что это UB.

            0
            Представьте, что вы написали выражение, в которое вовлечены одни константы, представленные по разному, 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 и прочее и прочее.

                +6

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

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

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

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

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

                          0
                          в х64 это выражение преобразовывается в ptrdiff_t и отрабатывает правильно… Я вообще на живой студии запускал, как этот сайт работает я не знаю.
                            0

                            Вы же про первый пример из статьи? Там по стандарту приведение к unsigned int должно быть. Я понимаю, что MSVC не всегда строго следует стандарту, но ptrdiff_t там как-то совсем неоткуда взяться.


                            Чисто из интереса, можете привести код, который вы собираете, и с какой ошибкой он сваливается?


                            Сразу хочу усомниться в «полезности» этого онайн-компилятора, раз результат компиляции отличается от реальной студии. Для тестирования перенсоимости кода это сайт явно не походит.

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

                              0
                              в последней версии MSVC создал консольный х86 проект, ничего в настройках не менял, скопировал верхний пример и попытался скомпилировать. Полный код и текст ошибки в первом комментарии с статье.
                              Так то все знают, что у студии своё понимание стандарта и кое что они делают не очень по стандарту… Но тут же речь не о соответствии стандарту, а о проверке того, как это компилируется в студии. Очевидно, что реальная MSVC более достоверна, чем её «эмуляция» на сайте. Интересно, что приведённые мной онлайн-компиляторы этот пример скомпилировали и выполнили, а студия выдаёт ошибку. Я в дебаге компилировал, разумеется.
                                +2
                                Compiler Warning (level 2) C4308

                                Ага, теперь всё более понятно. Такое предупреждение и на godbolt повторяется, если /W2 добавить. И ожидаемо одинаково выглядит на x86 и x64.
                                Видимо, студия ещё и некоторый аналог -Werror по умолчанию добавляет.


                                Вообще конечно полезное предупреждение, в gcc и clang тоже было бы не лишним (-Wsign-conversion всё-таки иногда слишком широкое, поэтому по умолчанию и выключено).


                                Очевидно, что реальная MSVC более достоверна, чем её «эмуляция» на сайте.

                                Я бы, конечно, проверил в VS, но её как-то совсем неудобно без винды запускать. А godbolt скорее всего виртуалки с вполне реальным MSVC использует.

                                  0
                                  Разобрался, почему студия не компилирует. По умолчанию включена опция SDL check. Видимо на сайте этаоция выключена, поэтому появляется предупреждеие, которое никто не читает. Если SDL check включено, то это предупреждение пропустить не получится. Так что, по умолчанию, MSVC пытается не дать отстрелить ногу :)
                            0
                            Сразу хочу усомниться в «полезности» этого онайн-компилятора, раз результат компиляции отличается от реальной студии. Для тестирования перенсоимости кода это сайт явно не походит.
                              +1

                              В проекте студии есть некоторый набор флагов по-умолчанию, включая тот же /W2. Еще и какой-нибудь <windows.h> и прочих windows-специфичных заголовочников включает в область видимости проекта. Попытайся вы собирать каким-нибудь cl.exe из консоли столкнулись бы с тем же, что и на godbolt - без указания флагов предупреждения не видать.

                                0
                                Но это уже сайт «не соответствует» студии, а не живая MSVC не соответствует сайту.
                                Да, мне стоило сразу уточнить, что в MSVC всё по умолчанию, включая компилятор и собирался проект через IDE, а не в консольном режиме. И стандарным компилятором, ведь ничего же не мешает любой компилятор к IDE поключить.
                                  0

                                  Сайт заявлен как онлайн-компилятор с возможностью видеть результат компиляции в виде asm кода. Было бы странно, если бы на каждый отдельный запущенный кусок cpp кода приходилось запускать целую IDE, как-то настраивать проект и прочее. Тут и с доставкой самого файла проекта вопрос встаёт и с кастомизацией его параметров. Да и вечный вопрос а какие настройки по-умолчанию накатываются тоже как бы есть - собирая проект не знаешь наверняка как именно собирается проект и чего напихают в финальный бинарь. В этом плане голый компилятор - то что доктор прописал. Максимальный контроль над тем что и как будет собрано. The true way как многие опытные люди говорят. Поэтому сайт не пытается "соответствовать" чьим-то каким-то настройкам где-то в какой-то IDE, а дает смотреть результат нескольких десятков голых компиляторов, среди которых есть и msvc, при помощи единого "консольного" интерфейса. На студии мир не сошёлся.

                              0
                              www.onlinegdb.com/online_c++_compiler
                              www.programiz.com/cpp-programming/online-compiler
                              cpp.sh

                              взял из выдачи гугла первые три онлайн-компилятора, они тоже "-4" возвращают.
                    0

                    Моя специфика работы подразумевает отсутствие неявных преобразований типов и самовыведений компилятором. Скорее всего будет одна int8_t константа и там по месту будет приводиться явно к нужному типу, через static_cast.

                      0
                      Не доверяете компилятору? Зачем тогда он нужен?
                        0

                        Потому что с++ язык со слабой типизацией. Он позволяет сделать плохие вещи, которые приводят к багам.

                        Это не недоверие компилятору, это особенность языка.

                    +4
                    Кастить -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 в действии? Ну ОК тогда.

                      –1
                      Компилятор транслирует код из С++ через несколько представлений в код процессора (читай Ассемблера). А у Ассемблера правил меньше, но они чёткие и ясные. И делить -3 на 3u командой DIV сразу вылезет боком.
                      Есть еще теория компиляторов. Там за такие преобразования сразу с экзамена долой.
                      Есть еще и другие языки программирования. На них такого поведения не наблюдается.

                      Хорошо задокументированная бага становится фичей! Многие не видят уже багу, а видят фигу фичу.
                        +1
                        Во-первых, есть жизнь за пределами x86, и команда деления (тем более две разных) есть далеко не у всех процессоров.
                        Во-вторых, если вы хотите получить конкретный ассемблерный код — вам никто не мешает писать его прямо на ассемблере, в т.ч. вставкой в коде на Си/C++.
                        То, что другие языки не похожи на Си и C++, как раз и мотивирует существование и тех и других.
                          0
                          Компилятор транслирует код из С++ через несколько представлений в код процессора (читай Ассемблера).

                          Ну да, ну да...


                          А у Ассемблера правил меньше, но они чёткие и ясные.

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


                          И делить -3 на 3u командой DIV сразу вылезет боком.

                          Вы удивитесь возможно, но делить можно совсем без DIV инструкций.
                          Я уж умолчу когда C-код компилится во что-нибудь другое, не нативное, типа LLVM (clang -emit-llvm), не говоря уже про экзотику типа WASM и т.д. и т.п.


                          Есть еще теория компиляторов. Там за такие преобразования сразу с экзамена долой.

                          У нас с вами разные профессора по видимому преподавали — мне например объясняли почему так и зачем оно может быть нужно.
                          А может и вам объясняли да позабылось, эта картинка ничего не напоминает?
                          image


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

                          Правда?!.. Дайте угадаю, вы пришли в С/С++ из так называемых "высокоуровневых" языков?
                          А по теме если — ну вот при чём тут другие языки? В качестве примера разделите -300001 на 50000 на сях или плюсах и затем на вашем любимом "другом" языке.


                          Вероятно картина будет следующая...
                          // C/С++
                          -300001/50000 = -6
                          -300001%50000 = -1
                          --------------------
                          -6*50000 + -1 = -300001
                          
                          // некий другой язык:
                          -300001/50000 = -7
                          -300001%50000 = 49999
                          --------------------
                          -7*50000 + 49999 = -300001

                          Подсказка — ни то, ни другое не является неправильным, это просто разные подходы (а именно remainder vs. residual arithmetic), и обоим есть соответствующее обоснование в парадигмах конкретного языка.


                          Хорошо задокументированная бага становится фичей! Многие не видят уже багу, а видят фичу.

                          Это не бага от слова совсем. Если вы не понимаете зачем это нужно, это не значит, что это сразу внезапно ошибка.
                          Ну почитайте классику в конце-то концов.


                          В качестве примера вопрос для "на подумать" — у вас есть две целочисленные переменные фиксированной (что важно), но при этом разной разрядности (ибо одно грубо говоря на 1 бит короче), которые вы делите или умножаете друг на друга… Как наиболее оптимально сделать это без объявления такого действия UB, максимально используя их разрядность (или оставаясь в заданной размерности), при этом согласуя их conversion rank и т.п., чтобы в большем числе случаев результат оказался предсказуемо "хорошим"? (заметьте не математически правильным, ибо это просто нереально при вероятном переполнении)… Другими словами — как оттянуть тот момент переполнения?

                            0
                            Подсказка — ни то, ни другое не является неправильным, это просто разные подходы (а именно remainder vs. residual arithmetic), и обоим есть соответствующее обоснование в парадигмах конкретного языка.

                            Интересно, какие есть обоснования для первого подхода, кроме того, что его было проще реализовывать в железе n-цать лет назад?

                              0
                              Например, именно первый подход учат в школьной арифметике.
                                0

                                Точно? Там учат получать отрицательные остатки? Я смутно помню определение с числовой осью, где берется ближайшая кратная точка слева и расстояние до нее — остаток. Хотя не исключаю, что последующие знания полей по модулю вытеснили старое определение. И теперь отрицательные остатки кажутся какой-то дикостью.

                                  0
                                  И теперь отрицательные остатки кажутся какой-то дикостью.

                                  Ну хорошо, -13/4 даёт у вас остаток 3, а 13/-4 даёт остаток -3 — это не дикость? А по F-делению именно так (можете проверить на Питоне).
                                  Тогда уж лучше требовать E-деление ("евклидово"), у которого независимо от знаков делимого и делителя остаток таки всегда неотрицательный. Есть и языки с ним.

                                    0
                                    13/-4 даёт остаток -3 — это не дикость?

                                    Дикость, конечно. Я же говорю: мне "отрицательные остатки кажутся какой-то дикостью".


                                    И да, евклидово деление было бы лучше всего, но даже F-деление лучше. Оно, по крайней мере, при делении на положительное всегда дает нормальные остатки и математика по вычетам при нем работает без костылей. А остаток от деления на отрицательное — очень редкая операция.

                          0
                          Потому что человек вдруг увидел usual arithmetic conversions в действии?

                          Даже с 20 годами опыта легко расслабиться и пропустить подобную диверсию где-то в коде, особенно когда это осложнено макрами, шаблонами и прочими факторами, которые сбивают очевидность, рассеивая внимание и делая неочевидными влияющие факторы (где какой тип).
                          Шутки типа "участие unsigned int приводит к беззнаковой операции, а участие unsigned short — нет, ибо он вначале конвертируется в signed int" ещё усложняют это.

                            –1
                            Даже с 20 годами опыта легко расслабиться и пропустить подобную диверсию где-то в коде, особенно когда это осложнено макрами, шаблонами и прочими факторами, которые сбивают очевидность, рассеивая внимание и делая неочевидными влияющие факторы (где какой тип).

                            Ну да, странно же в языках типа C/C++ с целочисленными типами фиксированной разрядности… где и переполнение точно таким же образом словить можно (вас не смущает например что (char)127+2 == -127?), и несколько "странный" остаток при делении отрицательного на положительное число (см. remainder vs. residual arithmetic) и т.п.


                            Я стесняюсь спросить а те 20 лет опыта точно в C/C++?
                            Кроме того ну есть же warnings, тестовое покрытие и т.д.


                            Шутки типа "участие unsigned int приводит к беззнаковой операции, а участие unsigned short — нет, ибо он вначале конвертируется в signed int" ещё усложняют это.

                            Ничего оно тут не усложняет — приведение типов к большей разрядности это тоже часть usual arithmetic conversions и это просто тупо — вопрос приоритетов.

                              0
                              вас не смущает например что (char)127+2 == -127

                              Ну вас не смущает то, что, например, при -funsigned-char (GCC) это будет не -127? Давайте уже говорить про адекватно определённые целые типы.


                              и несколько "странный" остаток при делении отрицательного на положительное число (см. remainder vs. residual arithmetic)

                              Я не думаю, что T-деление более странное, чем F-деление. У каждого своя ниша, да. Но по достаточно легко находимым причинам T-деление заметно чаще в железе.


                              Я стесняюсь спросить а те 20 лет опыта точно в C/C++?

                              Это был типа намёк такой что нефиг говорить про большой опыт? Это у вас не лучший приём, вообще-то, но отвечу конструктивно: может быть, что и при 20 лет опыта в C и C++ кто-то ни разу не нарывался на такое. Ну вот другого рода задачи у него были. Тут важнее кругозор (который в том числе развивается чтением и обсуждением таких статей, как текущая).


                              Кроме того ну есть же warnings, тестовое покрытие и т.д.

                              И вот именно тот кто реально хорошо знает C/C++ знает, что ни ворнинги, ни тесты не помогут против такого, когда компилятору вдруг взбрело в "голову" применить конкретную плюшку UdB или UsB. Я нисколько не претендую на гуру, но это как раз знаю. А вот ваши упоминания тестов и ворнингов начинают смущать...


                              Ничего оно тут не усложняет — приведение типов к большей разрядности это тоже часть usual arithmetic conversions и это просто тупо — вопрос приоритетов.

                              Оно уже изначально контринтуитивно. А дальше когда вы пишете, например, шаблон с участием unsigned случая и вдруг оказывается, что ещё надо явно преобразовать типы, которые у́же int, к unsigned, потому что иначе оно будет signed… слишком легко такое пропустить, просто расслабившись.

                                –1
                                Давайте уже говорить про адекватно определённые целые типы.

                                А давайте без давайте не передёргивать — что имелось ввиду я думаю понятно, ну или если хотите замените char на int8_t.


                                Я не думаю, что T-деление более странное, чем F-деление.

                                Кавычки на слове "странный" вы старательно не заметили по видимому. Если что это была попытка в сарказм.


                                ни тесты не помогут против такого, когда компилятору вдруг взбрело в "голову"

                                И как оно не проявится на тестах? Когда код покрывают тестами, оно должно быть для всех corner case. Если компилятору что то там "взбрело в голову", вплоть до UB, то ваши тесты должны это показать, иначе у вас покрытие как минимум не полное.


                                Оно уже изначально контринтуитивно.

                                Нет. Точка.
                                Оно оправдано и обосновано.

                                  0
                                  ну или если хотите замените char на int8_t.

                                  Ну хотя бы — уже будет что-то осмысленное.


                                  Если компилятору что то там "взбрело в голову", вплоть до UB, то ваши тесты должны это показать, иначе у вас покрытие как минимум не полное.

                                  Вы это всерьёз? Покажите где-то не в Палате мер и весов (или студенческой работе) полное покрытие — я имею в виду, не все ветки исполнения (это банально и ни о чём), а все возможные случаи входных значений.
                                  (Я уж не вспоминаю, что да, полное покрытие всех путей исполнения часто тоже нереальная задача, и что возможности по оптимизации и инлайнингу часто приводят к решениям, которые не покрываются проверкой модулей-кусочков даже на все значения аргументов.)


                                  Нет. Точка.

                                  Божественный аргумент. Главное, убедительный. :sarcasm:


                                  Оно оправдано и обосновано.

                                  То-то в новых языках массово отказываются от этого трахомудрия...

                              0
                              Согласен. Есть такое.
                              Проблема еще и в том, что я вначале выучил Ассемблер x86 и очень хорошо его знаю, а потом остальные языки, 6 профессионально и еще больше 20 понимаю и могу кодить. Но Ассемблер собака всё определяет. Почитал теорию компиляторов для практики и опять Ассемблер внёс свое. Что-то новое постоянно протряхивается через Ассемблер. Но какой код не мечтает стать Ассемблером (инструкциями процессора)?
                          +4
                          В отличии от деления, где знаковость влияет на результат, 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 может применяться к любым регистрам (но при этом не происходит расширение разрядности).

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

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

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

                                      Есть, но:


                                      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
                                    Если представить небольшое отрицательное число, например -1 (0xFF) как 0x100-0x1 и обозначим 0x100 как Т, то перемножение двух небольших отрицательных чисел A и B представится в виде:
                                    A * B = (T-a)*(T-b) = T^2 - T*a - T*b - a*b = T*(T-a-b) + a*b => a*b
                                    , здесь a = abs(A), b = abs(B)
                                    При сохранении ответа в той же разрядности, что и A и B, останется только a*b, так как умножение на Т сдвигает результат за пределы разрядности.
                                    Так что умножение знаковых чисел с MUL и беззнаковых IMUL даст правильный результат.

                                    Но деление — НЕТ!
                                    (T-a)/(T-b) = ?
                                      –1
                                      Так что умножение знаковых чисел с MUL и беззнаковых IMUL даст правильный результат.

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

                                        В IMUL 2-3-операндном разрядность произведения равна разрядности сомножителей (с усечением).
                                        Предыдущий комментатор выразился некорректно, но суть понятна: умножение N*N->N может быть сделано одинаково для обоих вариантов знаковости (если нет переполнений), а деление — уже нет.

                                    –1
                                    Этот пример показывает не про операцию умножения а про особенность деления знакового и беззнакового (и наоборот).

                                    mov dword ptr [rbp - 8], 3 // int i = 3;
                                    mov dword ptr [rbp - 12], 3 // unsigned u = 3;
                                    mov eax, dword ptr [rbp - 8]
                                    imul eax, dword ptr [rbp - 12] // eax = 9
                                    mov ecx, 4294967293
                                    xor edx, edx
                                    div ecx // 9 / 4294967293 = 0
                                    cmp eax, 0

                                    Этот DIV, он такой DIVный!!!
                                      +1
                                      Нет, это пример про то, что (i*u) имеет тип unsigned int, иначе был бы знаковое деление и знаковый результат.
                                    0
                                    «the unsigned operand is converted to the signed operand's type», разумеется при соблюдении ранга преобразования

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

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

                                        +6

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

                                          –1

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

                                            +3
                                            Я не уверен, что хотя бы 10% программистов читает стандарты, и уж тем более новые стандарты.
                                              0
                                              Этот топик как раз и подтверждает, что при неожиданном поведении компилятора программист смотрит в стандарт языка в последнюю очередь — хотя поговорка «If all else fails, read the instructions» намного старше компьютеров.
                                                –1
                                                Ну да, инструкцию обычно читаем после того как что-то сломалось. И если в инструкции Стандарта сказано, что пациента в морг, значит в морг. Без базара.
                                                А то что в Ассемблере такого и близко нет — это проблемы Ассемблера, хоть С++ и транслируется в него и вынужден следовать правилам Ассемблера.
                                                  +1
                                                  Если читать инструкцию перед использованием, то поломки можно избежать.
                                                  Это касается не только компиляторов.
                                                    +1

                                                    Только для C/C++ для слишком многих проблем эта инструкция выглядит как "в случае пожара нажмите кнопку 1 и через 0.5-1с, но не раньше и не позже, кнопку 2 дважды, а теперь на кнопке 1 отбейте лезгинку".
                                                    И это ещё хорошо, потому что для большинства UdB она звучит "вы должны не упасть с этого узкого мостика, а как — уже ваше дело", когда другие расширяют мост.
                                                    Ну да, когда-то и Empire State Building строили на высоте безо всякой страховки, кидаясь полукилограммовыми заклёпками. И почему сейчас никто так не хочет работать, просто странно… (сарказм, да)

                                            0
                                            Если, как в Rust, тупо запретить арифметические действия над несовпадающими типами, и пока не добавят явные касты в каждую строчку, код не скомпилируется — то ещё больнее.
                                              +1
                                              С этим есть теоретическая проблема. Она еще в языке Ада встречалась. Если под числами подразумеваются физические величины( а так часто случается), то метр * метр дают квадратный метр, а не метр. Или вектор * скаляр = вектор. Или 16 bit * 16 bit =~ 32 бита И тд. А Ваше утверждение равносильно тому, что операция умножения (и деления) всегда определена только строго внутри одного типа.
                                                0
                                                Это не ко мне, а к авторам Rust
                                                  0
                                                  Извиняюсь, конечно не Вам, а строгой типизации в принципе.
                                                  0

                                                  Если язык даёт возможность определить свои типы с правилами в стиле "sqmeter operator*(meter x, meter y)", то принципиальной проблемы уже нет.
                                                  База языка, да, работает с безразмерными числами.

                                                    +1
                                                    За переопределение операторов плюсы тоже критикуют. Нет в мире совершенства :(
                                                      +1
                                                      За переопределение операторов плюсы тоже критикуют.

                                                      Ну да, я видел такое, особенно от сторонников Go. Нефиг, мол, скрывать за простым "+" мегатонны библиотечных действий. А когда я спросил "а как же complex? а если мне вдруг векторы потребуются?" — был ответ "что есть в языке, то есть, а остальное от лукавого". Ну ладно, пусть лукавит… ;)

                                                    +1

                                                    let m = $operator * (int16, uint16) -> int32) $;
                                                    auto product = m (x,y);

                                                    +2

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

                                                      0
                                                      Вот только это будет уже другой язык.

                                                      В C и C++ давно есть "атрибуты", которые позволяют расширить текущий язык с сохранением всей обратной совместимости.


                                                      [[int_arith(checked)]]
                                                      int foo(аргументы) {
                                                          ...
                                                      }

                                                      и все сужения, при которых аргумент непредставим в множестве результата, будут генерировать ошибку (исключение C++).
                                                      Осталось провести это в стандарт (ещё 20 лет настойчивого давления).

                                                        0
                                                        www.boost.org/doc/libs/develop/libs/safe_numerics/doc/html/index.html уже давно существуют, и их можно использовать с любым компилятором C++.
                                                        Надо обязательно втащить это в стандарт? А весь остальной Boost случайно не хотите втащить?
                                                          +1
                                                          Надо обязательно втащить это в стандарт?

                                                          Да. Не должен такой фундаментальный аспект зависеть от внешней библиотеки, тем более что технически это сделать в базе — чрезвычайно дёшево по сравнению с прочими затратами. (Административная воля, да, в разы сложнее.)


                                                          А весь остальной Boost случайно не хотите втащить?

                                                          Половину или треть — да, вполне. Весь — вряд ли, там много такого, что требуется ну 0.1% от пользователей. Я понимаю ваши попытки сарказма, но они тут откровенно неуместны.

                                                            –1
                                                            Подход «всё, что нужно для работы, должно быть в стандартной библиотеке» имеет право на жизнь, и применяется например в Python: там в стандартной библиотеке есть криптохеши, JSON, base64, XML с XPath, HTTP-сервер и клиент, только чёрта в ступе нет.
                                                            Комитет C++ последовательно придерживается иного подхода.
                                                              +1
                                                              например в Python: там в стандартной библиотеке есть криптохеши, JSON, base64, XML с XPath, HTTP-сервер и клиент, только чёрта в ступе нет.

                                                              Правильная (строго определённая, логичная/предсказуемая, удобная и безопасная) работа стандартных операций над стандартными типами данных, как целые, не имеет ничего общего с перечисленным и должна быть в базе языка.


                                                              Комитет C++ последовательно придерживается иного подхода.

                                                              Слово "последовательно" здесь, очевидно, является высшей степенью сарказма. На шутку дня ваша реплика определённо удалась.

                                                      0

                                                      Это всё надо решать контекстными настройками (как checked/unchecked в C#, но с бо́льшим количеством вариантов), и по умолчанию разрешать расширительные конверсии, но не сжимающие или меняющие границы (как intN<->uintN). И для конверсий с отсутствием значения в выходном множестве давать варианты реакции на неконвертируемые значения (усечение, генерация ошибки, выбор ближайшего...)
                                                      Для C/C++ это возможно сделать [[атрибутами]], только надо стандартизовать их. Даже если умолчание останется по-старому, новые возможности помогут качеству кода.
                                                      (Вам лично, возможно, я что-то подобное уже писал, тут оставляю для всех.)

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

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

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

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

                                                            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.
                                                              +3
                                                              gcc 10.2.0 выдает предупреждение только если добавить -Wsign-conversion.
                                                              При этом -Wall -Wextra -Wpedantic не выдают ничего. (На -Wpedantic иронично конечно надеяться в данном случае)
                                                                0
                                                                Отлично. Теперь надо и для остальных случаев, типа float->bool, char* -> bool и прочих целых.

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

                                                                В стандарты это включено? А то на одном компиляторе предупредит, а на другом — нет. Вот чем комитет стандартизации должен заниматься.
                                                                  +2
                                                                  -Wconversion покрывает такие случаи.
                                                                    0
                                                                    проверил только что g++ 8.3.0 и 10:
                                                                    bool charbool = "true"; float floatbool = charbool;

                                                                    не выдаёт никаких предупреждений
                                                              +1

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

                                                                +2
                                                                В существующем ныне C++ — не появляется.
                                                                0
                                                                Хорошо, возьмём воображаемый «исправленный C++», в котором действие над int32_t и uint32_t считается знаковым.

                                                                Правильно "исправленный" C++ должен был выдать "у вас типы разной знаковости, унифицируйте", а не "считается знаковым".


                                                                Получим 1000000000*3u/3 == -431655765, тогда как сейчас получаем 1000000000.

                                                                А если 1000000000*5u/5, то почему-то и сейчас получим 141006540 — вместо генерации ошибки.


                                                                В каком из двух случаев «тупое следование явно нелогичным правилам»?

                                                                В обоих.

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


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

                                                                Я из налоговой. Какой говорите заработок?

                                                                  0
                                                                  Узнаю родную налоговую. Как в минусе — приглядывается. Тока что-то всплыло в плюсе, хоть и ошибочно, тут как тут!
                                                              +2
                                                              Странно, на носу уже с++23 маячит (опять туда напихают непонятно чего), но ни у кого руки не дойдут старое лигаси из стандарта вычистить…

                                                              Не совсем по теме, но очень близко: туда «напихивают» и кое-что понятное.
                                                              +2
                                                              Заметил, что это правило почему-то работает только для операции деления, а операция умножения вычисляется правильно.

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

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

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

                                                                    0
                                                                    -131071
                                                                      +2
                                                                      Знаковое переполнение — UB, так что теоретически тут может быть все что угодно.
                                                                        +2

                                                                        Знаковое переполнение при беззнаковой операции — вот веселуха, кстати.
                                                                        Integer promotion делается по правилу uint16 → int32.

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

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

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

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

                                                                              +3
                                                                              §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 — это ошибка компиляции.

                                                                                +1

                                                                                Там все немного хитрее:


                                                                                In Java SE 8 and later, you can use the int data type to represent an unsigned 32-bit integer, which has a minimum value of 0 and a maximum value of 2^32-1. Use the Integer class to use int data type as an unsigned integer.
                                                                                Static methods like compareUnsigned, divideUnsigned etc have been added to the Integer class to support the arithmetic operations for unsigned integers.

                                                                                In Java SE 8 and later, you can use the long data type to represent an unsigned 64-bit long, which has a minimum value of 0 and a maximum value of 2^64-1. Use this data type when you need a range of values wider than those provided by int. The Long class also contains methods like compareUnsigned, divideUnsigned etc to support arithmetic operations for unsigned long.
                                                                                +1
                                                                                C# и Java индексация — это int,

                                                                                Это решение проблемы по методу страуса. "Благодаря" этому индекс массива ограничен 31 битами.

                                                                                  +1

                                                                                  Я более того скажу: в C# по умолчанию ещё и сам размер массива ограничен 2GB (если элемент занимает больше памяти, тогда уменьшается максимальное число элементов).


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

                                                                                    0

                                                                                    А что было бы, если бы мысль сделать индексацию всегда по int пришла бы авторам языка во время 16-битных процессоров?


                                                                                    Вроде как для массивов свыше 2 Гб достаточно ключа в настройках сборки, код менять не надо.


                                                                                    а экономия на разрядности
                                                                                    Однако если нам нужна экономия на разрядности, используем с/с++ а не c#
                                                                                      0
                                                                                      А что было бы, если бы мысль сделать индексацию всегда по int пришла бы авторам языка во время 16-битных процессоров?

                                                                                      Тогда сейчас бы был не C#, а D#.


                                                                                      А ещё, внезапно, в C++ размер типа int может быть любым. В старые добрые времена int был 16-битным, индексация — тоже 16-битной. Ну и сложные указатели (16-битный сегмент + 16-битное смещение).

                                                                                0
                                                                                Перепишем вот так (безопасно, ибо UINT_MIN=0):
                                                                                for (size_t i = N; i >= UINT_MIN; i--)


                                                                                А этот опасен?:
                                                                                for (int i = N; i >= INT_MIN; i--)
                                                                                –1
                                                                                На том свете отосплюсь. И -3/3*3 тестировал. Мне понравилось. И компилятору тоже. А вот -3/3u*3 компилятор плохо переварил, но многим вижу и это понравилось. Вот и не сплю спокойно с тех пор.
                                                                                Кстати, чем тип -3 меньше типа 3?
                                                                                И делить можно только то, что разрешил Стандарт?
                                                                                +4

                                                                                Если бы в заголовке увидел каст результата к 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?
                                                                                          0

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

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

                                                                                            Если вы пишете 1000/x, то имеете в виду знаковое деление, а если 1000u/x — то беззнаковое, не так ли?


                                                                                            Чем должна определяться знаковость операции и типа результата — типом делимого, типом делителя, обоими?


                                                                                            Если, как предлагают «исправить C++», оба этих варианта будут обозначать знаковое деление

                                                                                            Предложение, как я понял, было в замене умолчания: после integral promotions, если операнды разной знаковости, то приводятся к знаковому (а сейчас — к беззнаковому). Случай одинаковой знаковости в нём не трогался совсем.


                                                                                            Текущие правила действительно жутковаты — integral promotions сначала, если расширяют, то к int (знаковому), потом приводят к общему с приоритетом на беззнаковость. Это может запутать даже спеца со стажем, если не держать постоянно в голове.


                                                                                            В языках, рождённых 15 лет назад и менее, от этого максимально уходят (уже в Go эти integral promotions устранены).

                                                                                              0
                                                                                              Если вы пишете 1000/x, то имеете в виду знаковое деление, а если 1000u/x — то беззнаковое, не так ли?

                                                                                              И в нынешнем C++ так и есть. В чём проблема?

                                                                                              Если, как предлагают «исправить C++», оба этих варианта будут обозначать знаковое деление

                                                                                              Предложение, как я понял, было в замене умолчания: после integral promotions, если операнды разной знаковости, то приводятся к знаковому (а сейчас — к беззнаковому).

                                                                                              Дело в том, что после замены умолчания не будет способа обозначить беззнаковое деление.
                                                                                                0
                                                                                                Дело в том, что после замены умолчания не будет способа обозначить беззнаковое деление.

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


                                                                                                И в нынешнем C++ так и есть. В чём проблема?

                                                                                                Прочитайте, пожалуйста, весь посыл моего сообщения, прежде чем комментировать.

                                                                                                  0
                                                                                                  «Дело в том», что я как раз говорил и говорю, что даже в этом случае остаётся нормальным вариантом, что деление беззнаковое при обоих беззнаковых аргументах.

                                                                                                  Именно об этом я и писал в комментарии, на который вы отвечали: «Явным кастом делимого, как в Rust?»

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

                                                                                                  Это уже совсем другое, нежели замена умолчания на противоположное.
                                                                                                  Если у запрета умолчания я вижу преимущества и недостатки, то у замены на противоположное — только недостатки.
                                                                                            +2

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

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

                                                                                                У gcc есть -Wsign-compare

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

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

                                                                                                      0
                                                                                                      Ну я предпочитаю выставлять уровень предупреждений по максимуму. Тут единственный косяк который я вижу — это то что флаг не входит в распространенные шорткаты -Wall / -Wextra. Но скажем у MSVC такой проблемы нет, предупреждение выдается без дополнительных манипуляций.
                                                                                                  –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. То есть для маленьких типов всё так, как хочет автор.
                                                                                                        +3
                                                                                                        — Чему равно -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!» Вот удивимся тогда, как мало мы тестируем и как много нам открытий чудных приносит компилятор друг.


                                                                                                          Слабая типизация не должна быть по умолчанию. Сделайте директиву, отключающую типизацию, где это необходимо, а потом включающую. И старый код, после простой доработки, будет совместим с новым стандартом. При этом м.б. найдны баги, и спасено несколько Боингов.
                                                                                                            0
                                                                                                            Кому нужна слабая типизация по умолчанию, те пишут на одних языках; кому нужна строгая типизация по умолчанию, те пишут на других языках. Возможность выбора — это прекрасно.
                                                                                                              +1
                                                                                                              Согласен, что «Возможность выбора — это прекрасно»! Однако у тех, кто нанят поддерживать старый софт, нет выбора. ИМХО надежная типизация должна стать международным стандартом.
                                                                                                                0
                                                                                                                Тем, кто нанят поддерживать старый софт, новые стандарты и новые компиляторы не помогут: старый софт написан по старым стандартам и для старых компиляторов.
                                                                                                                  0
                                                                                                                  ИМХО хорошие компиляторы должны быть совместимы с своими предыдущими версиями. Конечно, не всякий начальник одобрит переход на новый компилятор только для фиксации багов типизации. Но очень вероятно, что новый компилятор выдаст более оптимальный код и сам будет работать быстрее, а это уже серьезный довод для начальника. Затраты на переход со вставкой директив будут значительно меньше, чем написать весь код занаво.
                                                                                                                    +1

                                                                                                                    Как минимум это должно быть не директивами, а общей опцией сборки. Типа, ставим всем "режим C99" и чтобы оно не рыпалось.
                                                                                                                    Менять исходники всего, включая сторонние библиотеки, автогенерируемый код и т.п. — слишком тяжело, не одобрят.


                                                                                                                    И без этого переход на новый компилятор это очень часто неожиданные баги или отказы компилировать, и прорабатываются годами. А вы предлагаете ещё код менять… а спецов анализировать все древние хаки — кто найдёт?

                                                                                                                      0
                                                                                                                      Как справедливо отмечено в статье:
                                                                                                                      Боингов еще много летает.
                                                                                                                      Тоскливо…
                                                                                                                        0
                                                                                                                        Боингов еще много летает.

                                                                                                                        Хех, а чуть в сторону — кто скажет, что там с Airbus? А то я слышал, что у них за счёт политики "мы не продаём, а сдаём в аренду" всё в разы более закрыто, и что то, что у Boeing всплывает, у них слишком легко прятать… не знаю, как это гуглить.

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

                                                                                                                  Кстати, если использовать list initialization, которое рекомендуется к применению, которое не разрешает преобразование целочисленных типов, если в принимающий тип не влезает итоговое значение, то GCC и Clang выдают предупреждение:


                                                                                                                  ucast.cpp:4:38: error: constant expression evaluates to 4294967292 which cannot be narrowed to type 'int' [-Wc++11-narrowing]
                                                                                                                      std::cout << "-3/3u*3 = " << int{-3/3u*3} << std::endl;
                                                                                                                    0
                                                                                                                    Все неявные приведения к типу — зло и дорога к проблемам
                                                                                                                      0
                                                                                                                      в яве есть простое правило — результат приводится к наиболее простому типу. Если применить к вашему примеру — то логично, что результат unsigned, сами явно написали «u». А так, читаешь коменты — как будто ктулху вызываете)
                                                                                                                        0

                                                                                                                        1) Включайте все проверки компилятора по максимуму.

                                                                                                                        2) Не смешивайте разные типы в одном выражении.

                                                                                                                        3) А особенно не смешивайте знаковые и беззнаковые типы.

                                                                                                                        4) И вообще, не пользуйтесь беззнаковыми типами без крайней необходимости (например, для ьитовых операций). С чего вы взяли, что это тип "по умолчанию?". Послушаем что на этот счёт говорит Страуструп:

                                                                                                                        ES.102: Use signed types for arithmetic

                                                                                                                        Reason

                                                                                                                        Because most arithmetic is assumed to be signed; x - y yields a negative number when y > x except in the rare cases where you really want modulo arithmetic.

                                                                                                                        Example

                                                                                                                        Unsigned arithmetic can yield surprising results if you are not expecting it. This is even more true for mixed signed and unsigned arithmetic.

                                                                                                                          0
                                                                                                                          4) И вообще, не пользуйтесь беззнаковыми типами без крайней необходимости

                                                                                                                          Увы, стандартная библиотека C++ принуждает к использованию беззнаковых типов.
                                                                                                                          Почему размеры коллекций решили сделать size_t, а не ssize_t — непонятно.

                                                                                                                            0

                                                                                                                            Есть такое, у Страуструпа в документе по ссылке где-то упомянут этот досадный момент.

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

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