Нарушит ли это язык или существующий код, если мы добавим безопасные сравнения signed / unsigned с C / C ++?

StackOverflow https://stackoverflow.com/questions/3476590

Вопрос

После прочтения этого вопроса о сравнении подписанных и неподписанных (я бы сказал, они появляются каждые пару дней):

Я задавался вопросом, почему у нас нет надлежащих подписанных неподписанных сравнений, а вместо этого этот ужасный беспорядок?Возьмите выходные данные из этой небольшой программы:

#include <stdio.h>
#define C(T1,T2)\
 {signed   T1 a=-1;\
 unsigned T2 b=1;\
  printf("(signed %5s)%d < (unsigned %5s)%d = %d\n",#T1,(int)a,#T2,(int)b,(a<b));}\

 #define C1(T) printf("%s:%d\n",#T,(int)sizeof(T)); C(T,char);C(T,short);C(T,int);C(T,long);
int main()
{
 C1(char); C1(short); C1(int); C1(long); 
}

Скомпилированный с помощью моего стандартного компилятора (gcc, 64bit), я получаю следующее:

char:1
(signed  char)-1 < (unsigned  char)1 = 1
(signed  char)-1 < (unsigned short)1 = 1
(signed  char)-1 < (unsigned   int)1 = 0
(signed  char)-1 < (unsigned  long)1 = 0
short:2
(signed short)-1 < (unsigned  char)1 = 1
(signed short)-1 < (unsigned short)1 = 1
(signed short)-1 < (unsigned   int)1 = 0
(signed short)-1 < (unsigned  long)1 = 0
int:4
(signed   int)-1 < (unsigned  char)1 = 1
(signed   int)-1 < (unsigned short)1 = 1
(signed   int)-1 < (unsigned   int)1 = 0
(signed   int)-1 < (unsigned  long)1 = 0
long:8
(signed  long)-1 < (unsigned  char)1 = 1
(signed  long)-1 < (unsigned short)1 = 1
(signed  long)-1 < (unsigned   int)1 = 1
(signed  long)-1 < (unsigned  long)1 = 0

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

long:4
(signed  long)-1 < (unsigned   int)1 = 0

Ответ на вопрос "Как?" из всего этого легко найти:Просто перейдите к разделу 6.3 стандарта C99 или главе 4 C ++ и найдите предложения, которые описывают, как операнды преобразуются в общий тип, и это может нарушиться, если общий тип переосмысливает отрицательные значения.

Но как насчет вопроса "Почему?".Как мы можем видеть, '<"выходит из строя в 50% всех случаев, также это зависит от конкретных размеров типов, так что это зависит от платформы.Вот несколько моментов, которые следует учитывать:

  • Процесс преобразования и сравнения на самом деле не является ярким примером применения Правила наименьшего удивления

  • Я не верю, что где-то существует код, который опирается на утверждение, что (short)-1 > (unsigned)1 и является нет написанный террористами.

  • Все это ужасно, когда вы работаете на C ++ с шаблонным кодом, потому что вам нужна магия признаков типа, чтобы связать правильный "<".


В конце концов, сравнивая знаковые и беззнаковые значения разных типов является простота в реализации:

signed X < unsigned Y -> (a<(X)0) || ((Z)a<(Z)b) where Z=X|Y 

Предварительная проверка дешева и также может быть оптимизирована компилятором, если a>= 0 может быть доказано статически.

Итак, вот мой вопрос:

Нарушит ли это язык или существующий код, если мы добавим безопасные сравнения signed / unsigned с C / C ++?

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


Обновить: Я запустил это на своем старом добром Turbo-C ++ 3.0 и получил такой результат:

char:1
(signed  char)-1 < (unsigned  char)1 = 0

Почему это (signed char)-1 < (unsigned char) == 0 здесь?

Это было полезно?

Решение

Да, это нарушило бы язык / существующий код.Язык, как вы заметили, тщательно определяет поведение, когда операнды со знаком и без знака используются вместе.Такое поведение с операторами сравнения необходимо для некоторых важных идиом, таких как:

if (x-'0' < 10U)

Не говоря уже о таких вещах, как (сравнение равенства):

size_t l = mbrtowc(&wc, s, n, &state);
if (l==-1) ... /* Note that mbrtowc returns (size_t)-1 on failure */

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

Другие советы

Мой ответ предназначен только для языка Си.

В C нет типа, который мог бы вместить все возможные значения всех возможных целочисленных типов.Ближе всего к этому подходит C99 intmax_t и uintmax_t, и их пересечение охватывает только половину их соответствующего диапазона.

Следовательно, вы не можете реализовать математическое сравнение значений, таких как x <= y путем первого преобразования x и y к общему типу, а затем выполняем простую операцию.Это серьезное отклонение от общего принципа работы операторов.Это также нарушает интуицию о том, что операторы соответствуют вещам, которые, как правило, являются отдельными инструкциями в обычном оборудовании.

Даже если бы вы добавили эту дополнительную сложность языку (и дополнительную нагрузку на разработчиков реализации), у него были бы не очень приятные свойства.Например, x <= y все равно это не было бы эквивалентно x - y <= 0.Если бы вам нужны были все эти приятные свойства, вам пришлось бы сделать целые числа произвольного размера частью языка.

Я уверен, что существует множество старого unix-кода, возможно, какой-то из них запущен на вашем компьютере, который предполагает, что (int)-1 > (unsigned)1.(Ладно, может быть, это написали борцы за свободу ;-)

Если вам нужен lisp / haskell / python /$favorite_language_with_bignums_built_in, вы знаете, где это найти...

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

Существует гораздо больше кода, написанного на C и C ++, чем мы с вами вместе можем себе представить (некоторые из них могут быть даже написаны террористами).

Полагаясь на "предложение , которое (short)-1 > (unsigned)1- это может быть сделано кем-то непреднамеренно.Существует множество C-кода, имеющего дело со сложными битовыми манипуляциями и подобными вещами.Вполне возможно, что какой-то программист использует текущее поведение сравнения в таком коде.(Другие люди уже предоставили хорошие примеры такого кода, и код даже проще, чем я ожидал).

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

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

c ++ 0x - и boost - являются примерами ужасного синтаксиса - ребенка, которого могут любить только его родители, - и находятся на очень большом пути от простого элегантного (но сильно ограниченного) c ++ 10-летней давности.

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

А когда-то сломанный, есть так много всего другого, что также подлежит ретроактивному исправлению.

Единственными способами для языка определить правила, которые могут приблизиться к соблюдению принципа наименьшей неожиданности во время выполнения при использовании объединения операндов разных типов языка C, было бы либо запретить компилятору неявные преобразования типов по крайней мере в некоторых контекстах (заменив "сюрприз" на "почему это не компилируется?" и снизив вероятность возникновения неожиданных ошибок в будущем), либо определить несколько типов для каждого формата хранения (например,как оберточные, так и не оберточные варианты каждого целочисленного типа), или и то, и другое.

Наличие нескольких типов для каждого формата хранения, напримеркак переносящие, так и не переносящие версии 16-разрядных целых чисел со знаком и без знака, могли бы позволить компилятору различать "Я использую здесь 16-разрядное значение на случай, если это повысит эффективность, но оно никогда не превысит диапазон 0-65535 и мне было бы все равно, что случилось, если бы это произошло)" и "Я использую 16-битное значение, которое нужно преобразовать в 65535, оно становится отрицательным".В последнем случае компилятор, который использовал 32-разрядный регистр для такого значения, должен был бы маскировать его после каждой арифметической операции, но в первом случае компилятор мог бы опустить это.Что касается вашего конкретного пожелания, смысл сравнения между длинной подписью без упаковки и без упаковки unsigned long было бы понятно, и для компилятора было бы уместно сгенерировать последовательность из нескольких команд, необходимую для того, чтобы это произошло (начиная с преобразования отрицательного числа в не обтекаемое unsigned long было бы неопределенным поведением, если бы компилятор определил поведение для операторов сравнения для этих типов, это не конфликтовало бы ни с чем другим, что могло бы быть указано).

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

Если бы сравнение между целочисленными типами сравнивало фактические математические значения, я бы хотел, чтобы то же самое происходило для сравнения между целыми числами и числами с плавающей запятой.И сравнить точные значения произвольного 64-битного целого числа и произвольного числа с плавающей запятой двойной точности довольно сложно.Но тогда компилятор, вероятно, справился бы с этим лучше, чем я.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top