Подтвердить что ты не робот

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

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

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

#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 на С++ и выкопайте предложения, которые описывают, как операнды преобразуются в общий тип, и это может сломаться, если общий тип переинтерпретирует отрицательные значения.

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

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

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

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


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

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

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

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

Может ли он сломать язык или существующий код, если мы добавим безопасные сопоставленные/неподписанные сравнения к C/С++?

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


UPDATE: Я запускал это на своем старом Turbo-С++ 3.0 и получил этот вывод:

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

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

4b9b3361

Ответ 1

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

if (x-'0' < 10U)

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

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

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

Ответ 2

Мой ответ только для C.

В 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, вы знаете, где его найти...

Ответ 3

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

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

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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

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

Ответ 6

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