Информация об изменениях

Сообщение Re[2]: Коды ошибок и отрицательные числа от 30.01.2022 8:42

Изменено 30.01.2022 8:53 netch80

Re[2]: Коды ошибок и отрицательные числа
Здравствуйте, 5.März, Вы писали:

5M>Выбор int как типа по умолчанию не было ошибкой, все что семантически представляет собой целое число (неважно, может быть отрицательным или нет) должно быть по умолчанию знаковым типом. Реальная разница signed и unsigned не в отрицательности, их просто назвали неудачными именами. Из-за неудачного названия программисты теперь суют unsigned во все, что не может быть отрицательным.


5M>Signed и unsigned следуют разной арифметике: signed представляет обычные числа которые следуют обычной арифметике в которой x-1 < x всегда true (именно из-за этого signed underflow/overflow навсегда объявлено UB), unsigned представляет машинные слова которые следуют modulo-2^N арифметике, где x-1 может быть как меньше так и больше x (well-defined)


Нет, это вы приводите как раз позднюю _ре_интерпретацию подхода, которая возникла по сути из-за хакерского переиспользования недостаточно строгих правил предшествующих стандартов.

Первые компиляторы C выполняли операции строго 1:1 и ничего почти не оптимизировали. Всякие сокращения вычисления констант, конечно, уже были, но современного уровня серьёзных переделок не было. Операции выполнялись так, как сейчас получается в GCC/Clang с -fwrapv: переполнение игнорировалось. Но: из-за одновременного наличия машин как минимум с дополнительным кодом (>99%) и обратным (отдельные модели), стандарт не смог записать это (скорее, к сожалению) для знаковых операций. Причём, такого жёсткого разделения между undefined, unspecified и implementation-defined behavior, как сейчас, тогда не было: термины вводились, но достаточно наобум. И высокой опасности эффектов от UdB не было, операциями с ними пользовались напропалую.

А уже в 1990-х, когда начали вводить высокий уровень оптимизаций, фактически как злые хакеры, "переиспользовали" лазейку в стандарте с UdB для signed для оптимизаций. Но это было на ~20 лет позже.

Всё описанное известно по документам и воспоминаниям. Ссылок в явном виде я не собирал, но если попадутся и вспомню — закину сюда. Если горит, можно, например, попросить khim@habr (с ходу нашлось такое очень близкое, но он ещё высказывался), или John Regehr, у него в блоге есть современные последствия, но и предпосылки наверняка исследовал.

Для современного состояния, конечно, ваши слова актуальны. К сожалению. Я бы таки предпочёл, чтобы вначале зафиксировались на усечении для всех случаев: это бы ускорило введение нормальных средств, в которых реакция на нарушение области значения (обычно называемое переполнением) задаётся конкретным символом операции или контекстом (лучше всего в среднем). Это бы ускорило тут прогресс лет на 20-25.

Для сравнения: в Ada разделены диапазонные целочисленные типы (вместе с базовыми стандартными знаковыми и беззнаковыми) и модульные. У диапазонных по умолчанию проверяется результат при присвоении (и при многих арифметических операциях), генерируется исключение при выходе за диапазон; это заметно дороже, но надёжно. Для модульного типа (в стиле type byte = modulo 2**8) все операции по модулю, и там по типу видно, что будет усечение. Для идеального языка я ожидаю что-то среднее: по умолчанию проверка с исключениями, но при явном указании программистом — правила упрощаются вплоть до того, как сейчас в C со знаковыми — считается, что программист всё предусмотрел и переполнения (и прочих нарушений) не будет.

5M>В 99.99% случаев когда ты размышляешь об операциях с числами, ты и читатели твоего кода подразумевают обычную, а не модульную арифметику, так что и тип нужно использовать предназначенный для чисел, а не для машинных слов.


5M>Поэтому C++ Core Guidelines, Google Style Guide и многие другие не рекомендуют использовать unsigned типы для чисел (только для флагов и битовых операций);


Они именно так мотивируют? можно цитату? (с ходу не нашёл)
А то я бы предположил скорее сложность задания реакции на переход 0 -> -1 (как в примерах из этого
Автор: netch80
Дата: 10.05.20
обсуждения). Тем более что вот прямая цитата:

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.


По-моему, вам пора прекратить мучить сову.

5M> Страуструп признал "беззнаковость" size_t исторической ошибкой; в новые стандарты введен ssize() возвращающий размеры в правильном типе.


"Исторической ошибкой" size() является, конечно, в некоторой степени, но это факт не unsigned самого по себе — потому что размер не может быть отрицательным, а, значит, домен правильный. Страуструп как раз объясняет причины, а вы как-то очень странно читаете:

1. Там, где размер участвует в арифметике — почти всегда — естественно могут появляться отрицательные значения. Да, он это говорит.

Но: проблемные последствия этого являются следствием устоявшейся кривости стандарта, который не предусматривает иного действия для беззнаковых, кроме как по модулю. Если бы s-10, где s как size() какого-то контейнера равно 4, вызывало бы исключение — проблемы бы не было (или было бы сильно меньше). Считаете невозможным такой результат? — ok, не конвертировать к знаковым, пусть выбивает. Возможно, отрабатывать? — явно конвертировать.

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

Жёсткий надёжный язык тут бы скорее отказался компилировать — приведите к одной знаковости или ступайте себе лесом. Чуть более расслабленный пересчитал бы множество значений результата и проинтерпретировал по нему — и это тоже имеет смысл. Но автоконверсия C/C++ это таки точно наследие "бородатых годов".

3. (Страуструп не писал вроде, но я добавлю Из-за (естественной) знаковости ptrdiff_t никакой контейнер байтов не может занимать более половины памяти (а с учётом кодирования в дополнительном коде — и ровно половина тоже недопустима).
Поэтому или разделять разные случаи (для байтовых одни правила, для прочих другие), или можно постановить, что size() любого контейнера не может превышать SSIZE_MAX.
Может, где-то есть платформа, где это даст неожиданные проблемы (например, 16-битный мир с одним массивом в адресах от 0x1000 до 0xEFFF), но их возможные проблемы проигнорировали, тем более что там и так непонятно, что делать — ptrdiff_t должен для корректной работы быть минимум 17-битным... на такой платформе и про STL сложно говорить всерьёз.

5M>C++ Core Guidelines: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-nonnegative

5M>Google Style Guide: https://google.github.io/styleguide/cppguide.html#Integer_Types
5M>B.Stroustrup “Subscripts and sizes should be signed”: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1428r0.pdf
5M>Длинное видео из 2013г где комитет C++ несколько раз признает ошибочность использования unsigned: https://channel9.msdn.com/Events/GoingNative/2013/Interactive-Panel-Ask-Us-Anything (12:10, 42:40, 1:02:50)

Последняя ссылка битая, видео, наверно, удалено.
По поводу остальных ссылок, удивительно, как вы при наличии прямых указаний авторов умудряетесь переинтерпретировать вплоть до полной противоположности.
Re[2]: Коды ошибок и отрицательные числа
Здравствуйте, 5.März, Вы писали:

5M>Выбор int как типа по умолчанию не было ошибкой, все что семантически представляет собой целое число (неважно, может быть отрицательным или нет) должно быть по умолчанию знаковым типом. Реальная разница signed и unsigned не в отрицательности, их просто назвали неудачными именами. Из-за неудачного названия программисты теперь суют unsigned во все, что не может быть отрицательным.


5M>Signed и unsigned следуют разной арифметике: signed представляет обычные числа которые следуют обычной арифметике в которой x-1 < x всегда true (именно из-за этого signed underflow/overflow навсегда объявлено UB), unsigned представляет машинные слова которые следуют modulo-2^N арифметике, где x-1 может быть как меньше так и больше x (well-defined)


Нет, это вы приводите как раз позднюю _ре_интерпретацию подхода, которая возникла по сути из-за хакерского переиспользования недостаточно строгих правил предшествующих стандартов.

Первые компиляторы C выполняли операции строго 1:1 и ничего почти не оптимизировали. Всякие сокращения вычисления констант, конечно, уже были, но современного уровня серьёзных переделок не было. Операции выполнялись так, как сейчас получается в GCC/Clang с -fwrapv: переполнение игнорировалось. Но: из-за одновременного наличия машин как минимум с дополнительным кодом (>99%) и обратным (отдельные модели), стандарт не смог записать это (скорее, к сожалению) для знаковых операций. Причём, такого жёсткого разделения между undefined, unspecified и implementation-defined behavior, как сейчас, тогда не было: термины вводились, но достаточно наобум. И высокой опасности эффектов от UdB не было, операциями с ними пользовались напропалую.

А уже в 1990-х, когда начали вводить высокий уровень оптимизаций, фактически как злые хакеры, "переиспользовали" лазейку в стандарте с UdB для signed для оптимизаций. Но это было на ~20 лет позже.

Всё описанное известно по документам и воспоминаниям. Ссылок в явном виде я не собирал, но если попадутся и вспомню — закину сюда. Если горит, можно, например, попросить khim@habr (с ходу нашлось такое очень близкое, но он ещё высказывался), или John Regehr, у него в блоге есть современные последствия, но и предпосылки наверняка исследовал.

Для современного состояния, конечно, ваши слова актуальны. К сожалению. Я бы таки предпочёл, чтобы вначале зафиксировались на усечении для всех случаев: это бы ускорило введение нормальных средств, в которых реакция на нарушение области значения (обычно называемое переполнением) задаётся конкретным символом операции или контекстом (лучше всего в среднем). Это бы ускорило тут прогресс лет на 20-25.

Для сравнения: в Ada разделены диапазонные целочисленные типы (вместе с базовыми стандартными знаковыми и беззнаковыми) и модульные. У диапазонных по умолчанию проверяется результат при присвоении (и при многих арифметических операциях), генерируется исключение при выходе за диапазон; это заметно дороже, но надёжно. Для модульного типа (в стиле type byte = modulo 2**8) все операции по модулю, и там по типу видно, что будет усечение. Для идеального языка я ожидаю что-то среднее: по умолчанию проверка с исключениями, но при явном указании программистом — правила упрощаются вплоть до того, как сейчас в C со знаковыми — считается, что программист всё предусмотрел и переполнения (и прочих нарушений) не будет.

5M>В 99.99% случаев когда ты размышляешь об операциях с числами, ты и читатели твоего кода подразумевают обычную, а не модульную арифметику, так что и тип нужно использовать предназначенный для чисел, а не для машинных слов.


5M>Поэтому C++ Core Guidelines, Google Style Guide и многие другие не рекомендуют использовать unsigned типы для чисел (только для флагов и битовых операций);


Они именно так мотивируют? можно цитату? (с ходу не нашёл)
А то я бы предположил скорее сложность задания реакции на переход 0 -> -1 (как в примерах из этого
Автор: netch80
Дата: 10.05.20
обсуждения). Тем более что вот прямая цитата:

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.


По-моему, вам пора прекратить мучить сову.

5M> Страуструп признал "беззнаковость" size_t исторической ошибкой; в новые стандарты введен ssize() возвращающий размеры в правильном типе.


"Исторической ошибкой" size() является, конечно, в некоторой степени, но это факт не unsigned самого по себе — потому что размер не может быть отрицательным, а, значит, домен правильный. Страуструп как раз объясняет причины, а вы как-то очень странно читаете:

1. Там, где размер участвует в арифметике — почти всегда — естественно могут появляться отрицательные значения. Да, он это говорит.

Но: проблемные последствия этого являются следствием устоявшейся кривости стандарта, который не предусматривает иного действия для беззнаковых, кроме как по модулю. Если бы s-10, где s как size() какого-то контейнера равно 4, вызывало бы исключение — проблемы бы не было (или было бы сильно меньше). Считаете невозможным такой результат? — ok, не конвертировать к знаковым, пусть выбивает. Возможно, отрабатывать? — явно конвертировать.

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

Жёсткий надёжный язык тут бы скорее отказался компилировать — приведите к одной знаковости или ступайте себе лесом. Чуть более расслабленный пересчитал бы множество значений результата и проинтерпретировал по нему — и это тоже имеет смысл. Но автоконверсия C/C++ это таки точно наследие "бородатых годов".

3. (Страуструп не писал вроде, но я добавлю: ) Из-за (естественной) знаковости ptrdiff_t никакой контейнер байтов не может занимать более половины памяти (а с учётом кодирования в дополнительном коде — и ровно половина тоже недопустима).
Поэтому или разделять разные случаи (для байтовых одни правила, для прочих другие), или можно постановить, что size() любого контейнера не может превышать SSIZE_MAX.
Может, где-то есть платформа, где это даст неожиданные проблемы (например, 16-битный мир с одним массивом в адресах от 0x1000 до 0xEFFF), но их возможные проблемы проигнорировали, тем более что там и так непонятно, что делать — ptrdiff_t должен для корректной работы быть минимум 17-битным... на такой платформе и про STL сложно говорить всерьёз.

5M>C++ Core Guidelines: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-nonnegative

5M>Google Style Guide: https://google.github.io/styleguide/cppguide.html#Integer_Types
5M>B.Stroustrup “Subscripts and sizes should be signed”: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1428r0.pdf
5M>Длинное видео из 2013г где комитет C++ несколько раз признает ошибочность использования unsigned: https://channel9.msdn.com/Events/GoingNative/2013/Interactive-Panel-Ask-Us-Anything (12:10, 42:40, 1:02:50)

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