Почему беззнаковые целые числа подвержены ошибкам?

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

Как они приводят к ошибкам безопасности? Может ли кто-нибудь ясно объяснить это, приведя подходящий пример?


person Destructor    schedule 22.05.2015    source источник
comment
Я бы решительно выступил за использование беззнаковых типов. Если вы неправильно понимаете условия цикла, вы плохой разработчик. Это очень простая математика, чтобы заставить ее работать с целыми числами без знака, и мне кажется гораздо более естественным, что величины беззнаковые.   -  person stefan    schedule 22.05.2015
comment
Проблема в том, что большинство разработчиков плохие...   -  person Joe    schedule 22.05.2015
comment
Они, безусловно, могут увеличить количество ошибок один за другим. Рассмотрим VLT, который наградил человека $2^32-1$ центами. thestar.com/news/ontario/2009/03/18/ Конечно, существует аналогичная проблема с числами со знаком, когда наименьшее отличается от наибольшего всего на единицу, но, поскольку мы часто играем около 0, край обрыва ближе с числами без знака.   -  person Theodore Norvell    schedule 22.05.2015
comment
Работая над старым CISC IBM System/38, мы, программисты низкого уровня, предпочитали использовать беззнаковые числа, поскольку беззнаковые арифметические действия приводили к исключению, если сложение/вычитание переполнялось, что помогало нам находить наши ошибки. Однако при отсутствии этой функции довольно легко заставить неподписанных молча испортить все довольно по-королевски.   -  person Hot Licks    schedule 22.05.2015
comment
Целые числа со знаком также подвержены ошибкам. Я потратил час на отладку проблемы в Java, когда смещение значения байта дало странные результаты. Это было связано с продвижением по службе и расширением подписи. Я бы предпочел иметь оба и выбрать правильный тип для работы.   -  person Matti Virkkunen    schedule 22.05.2015
comment
@MattiVirkkunen: Помимо подписанных и неподписанных, я бы предпочел иметь типы с явной семантикой переноса, явной проверочной семантикой, свободной семантикой mod 2ⁿ и семантикой переполнения-равно-UB. Разделение различных типов целых чисел позволило бы писать код, который является более переносимым, более надежным и более оптимизируемым, чем имеющийся с существующими сегодня типами и связанными с ними правилами [которые во многих случаях требуют, чтобы меньшие типы со знаком ведут себя с чистой семантикой упаковки, но позволяют математике на меньших беззнаковых типах генерировать неопределенное поведение].   -  person supercat    schedule 22.05.2015
comment
Аминь, @supercat. Семантику переполнения-равно-UB часто называют семантикой насыщения, а также семантику недополнения-равно-0.   -  person Doug McClean    schedule 22.05.2015
comment
@DougMcClean: под UB я имел в виду неопределенное поведение. Я не уверен, что существует достаточно случаев, когда насыщающая семантика более полезна, чем проверенная семантика, чтобы оправдать их включение в язык. На самом деле, я не уверен, как часто разрешение переполнения быть Undefined Behavior позволило бы действительно полезные оптимизации, которые также были бы недопустимы в соответствии со свободными стандартами модульной арифметики (которые диктовали бы, что если int составляет 32 бита, вычисления на int, которые переполняются должны давать значения, которые соответствуют правильным результатам по модулю 2³², но не должны вести себя как значения в пределах...   -  person supercat    schedule 22.05.2015
comment
...диапазон типа. Среди прочего, свободная семантика говорит, что при заданном int32_t x=INT32_MAX; x++; int64_t y1=x,y2=x; компилятору не требуется присваивать одно и то же значение y1 и y2, но для приведения y1 и y2 к uin32_t потребуется присвоить одно и то же значение, т. е. (INT32_MAX + 1u) . Я ожидаю, что явная семантика проверенных целых чисел могла бы позволить некоторые очень полезные оптимизации, если бы компилятору было разрешено проводить правильные вычисления за пределами указанной точности, и ему приходилось ловить только тогда, когда точность была потеряна. Учитывая icheck32_t w,x,y,z;, выражение w=x+y+z;...   -  person supercat    schedule 22.05.2015
comment
... потребуется только для получения правильного результата, если и x+y, и (x+y)+z могут быть представлены в icheck32_t, но компилятор может свободно ловить или нет на досуге, если x+y не может быть представлено, но x+y+z было.   -  person supercat    schedule 22.05.2015
comment
Семантика насыщения иногда может быть полезна для приложений обработки сигналов, но я понимаю, что вы имеете в виду.   -  person Doug McClean    schedule 23.05.2015


Ответы (8)


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

for(size_t i = foo.size(); i >= 0; --i)
    ...

Обратите внимание, что по определению i >= 0 всегда верно. (Во-первых, это вызвано тем, что если i подписано, компилятор предупредит о возможном переполнении с size_t из size()).

Упоминаются и другие причины: Опасность — здесь используются неподписанные типы!, самым сильным из них, на мой взгляд, является неявное преобразование типов между подписанным и беззнаковым.

person Ami Tavory    schedule 22.05.2015
comment
Я бы принял этот ответ, потому что это единственный, о котором компилятор не предупредит. - person Andriy Tylychko; 22.05.2015
comment
@AndyT Получите лучший компилятор. coliru.stacked-crooked.com/a/c79fc9148dfb5f3f - person Baum mit Augen; 22.05.2015
comment
@AndyT И кстати, мой пример на самом деле не получает предупреждения, в отличие от приведенного выше. :) - person Baum mit Augen; 22.05.2015
comment
@BaummitAugen: действительно :) - person Andriy Tylychko; 22.05.2015
comment
Время использовать operator--> ( перейти к ): for (size_t i = sz; i --> 0;) ... выполняет итерацию от sz-1 до 0 - person jingyu9575; 22.05.2015
comment
Это не демонстрирует проблему с целыми числами без знака. Это свидетельствует о проблеме с самим кодом. Пропаганда отказа от подходящих инструментов для работы из-за того, что они могут быть плохо использованы, никому не приносит пользы. Только не используйте их плохо. - person fyngyrz; 22.05.2015

Одним из важных факторов является то, что это усложняет логику цикла: представьте, что вы хотите выполнить итерацию по всем элементам массива, кроме последнего (что действительно происходит в реальном мире). Итак, вы пишете свою функцию:

void fun (const std::vector<int> &vec) {
    for (std::size_t i = 0; i < vec.size() - 1; ++i)
        do_something(vec[i]);
}

Выглядит хорошо, не так ли? Он даже компилируется чисто с очень высоким уровнем предупреждений! (Live) Итак, вы помещаете это в свой код, все тесты проходят гладко, и вы забываете об этом.

Теперь, позже, кто-то приходит и передает пустой vector вашей функции. Теперь с целым числом со знаком вы, надеюсь, заметили предупреждение компилятора sign-compare, представили соответствующий состав и не опубликовали код с ошибками в первую очередь.

Но в вашей реализации с целым числом без знака вы выполняете перенос, и условие цикла становится i < SIZE_T_MAX. Катастрофа, УБ и скорее всего крах!

Я хочу знать, как они приводят к ошибкам безопасности?

Это также проблема безопасности, в частности это переполнение буфера. Одним из возможных способов использования этого было бы, если бы do_something сделал что-то, что мог бы наблюдать злоумышленник. Возможно, они смогут определить, какие входные данные вошли в do_something, и таким образом данные, к которым злоумышленник не должен иметь доступа, будут просочены из вашей памяти. Это будет сценарий, аналогичный ошибке Heartbleed. (Спасибо храповому уроду за указание на это в комментарии .)

person Baum mit Augen    schedule 22.05.2015
comment
Меня всегда беспокоил этот предполагаемый контрпример. Это правда, что, просто взглянув на код близоруко, вы подумаете, что целые числа со знаком здесь лучше. Однако при этом игнорируется более крупная алгоритмическая проблема: алгоритм явно хочет специально обрабатывать последний элемент диапазона. Следовательно, этот алгоритм должен иметь какое-то предварительное условие или разветвление, которое фактически гарантирует, что диапазон имеет последний элемент! И с таким ветвлением целые числа без знака будут работать просто отлично. - person Kerrek SB; 22.05.2015
comment
@KerrekSB Это, вероятно, немного надумано, я согласен, но не совсем нереалистично: vector может быть членом класса, где последний элемент зависит от n предыдущего. Теперь, если n == 0, вам не нужен и последний элемент, поэтому вы оставляете vector пустым. Наш алгоритм никогда не обрабатывает последний элемент, поэтому у нас есть цикл, который я показал. Но я все же признаю, что такого никогда не было ни в одном коде, который я действительно видел (вероятно, потому что я написал большую часть кода, который я видел до сих пор, и я не делаю таких вещей). - person Baum mit Augen; 22.05.2015
comment
если do_something пропускает свои входные данные в мир, то у вас есть сценарий утечки данных (думаю, сердцебиение). - person ratchet freak; 22.05.2015
comment
@BaummitAugen: Опять же, используя нотацию полуоткрытого диапазона, вы бы выразили такую ​​​​зависимость, сказав, что элемент n зависит от элементов [0, n), и не было бы проблем с подпиской :-S - person Kerrek SB; 22.05.2015
comment
@KerrekSB Прежде всего, я не утверждаю, что эту ошибку нельзя исправить с помощью целых чисел без знака. Я также не говорю, что такие ошибки будут обычным явлением в хорошо спроектированном/написанном коде. Тем не менее, я чувствую, что то, что происходит с людьми, определенно не запрещено, я видел и более странные вещи. Это может быть не лучший пример, но если вы знаете лучший пример или хотите доказать, что Страуструп ошибается, я был бы рад увидеть ваш ответ. :) - person Baum mit Augen; 22.05.2015
comment
Почему здесь все должны использовать вычитание? Почему не for (std::size_t i = 0; i + 1 < vec.size(); ++i)? - person Siyuan Ren; 22.05.2015
comment
@SiyuanRen Я использовал вычитание, потому что это неправильно. Весь смысл этого вопроса и ответа заключается в том, чтобы выделить потенциальные ошибки. Никто не пытается утверждать, что эти ошибки нельзя исправить или избежать. Я просто утверждаю, что что-то подобное могло произойти, и это было бы плохо. Так что да, вы можете использовать свой код, а затем иметь правильный код. Дело в том, что кто-то может (легко) ошибиться (как я намеренно сделал в своем ответе). - person Baum mit Augen; 22.05.2015
comment
Опять плохой код. Неплохой тип переменной. Не делает дело. Целые числа не подвержены ошибкам. программирование подвержено ошибкам. - person fyngyrz; 22.05.2015
comment
@fyngyrz Ни Страуструп, ни кто-либо здесь не утверждают, что вы не можете писать правильный код с беззнаковыми типами. Мы все просто указываем на то, что есть ошибки, которые можно (более или менее) легко сделать с неподписанными типами, которые (в данном случае) по крайней мере вызвали бы предупреждение компилятора с подписанными типами. Опять же, дело не в том, что вы не можете сделать это правильно, а в том, что вы можете ошибаться. Это то же самое, что и с new[] против std::vector. - person Baum mit Augen; 22.05.2015
comment
@fyngyrz И, конечно же, программирование подвержено ошибкам. Мы ищем инструменты, которые предотвращают ошибки (в данном случае подписанные типы), потому что программирование подвержено ошибкам, и потому что люди делают ошибки. - person Baum mit Augen; 22.05.2015
comment
@fyngyrz: в системах, где максимальная длина объекта слишком велика, чтобы поместиться в подписанный int, но поместится в unsigned int, оптимальная семантика была бы получена, если бы size_t был типом, который хранится в формате unsigned-int, но продвигает в long при преобразовании rvalue. Наличие таких размеров с использованием одного слова, а не двух, выгодно, но существующие правила, которые заставляют вычисления с их участием оцениваться с использованием модульной арифметики, гораздо менее выгодны. - person supercat; 22.05.2015
comment
@fyngyrz: ИМХО, unsigned int - это идеальный тип переменной в случаях, когда нужно выполнить модульную арифметику, но это семантически неподходящий [неплохой] тип в случаях, когда кто-то представляет количества. - person supercat; 22.05.2015
comment
Вот что касается поиска инструментов, которые предотвращают ошибки (в данном случае подписанных типов), потому что программирование подвержено ошибкам, и потому что люди делают ошибки. Это приводит к созданию более слабых программистов и менее мощных инструментов. Это сложное ремесло, мастера в нем знают, что им нужно. - person fyngyrz; 23.05.2015
comment
о господи, 5-минутная правка меня СНОВА достала. да ладно, stackoverflow, в чем смысл этого? - person fyngyrz; 23.05.2015
comment
ищите инструменты, которые предотвращают ошибки (в данном случае подписанные типы)... Это создает более слабых программистов и менее мощные инструменты. Программирование — это и должно быть трудным ремеслом; мастера в нем знают, что делают. Не нужно смягчать его для учеников и любителей. Лучше научиться обращаться с доступными эффективными молотками и пилами. Не затупленные края и резиновые молотки. Make it easy сошла с рельсов. Просто напишите это правильно в первую очередь, вот и все. Создание навыков всегда лучше, чем разрушение инструментов. не? Используйте правильный размер uint. Правильно. Без проблем. - person fyngyrz; 23.05.2015
comment
@fyngyrz Это, вероятно, имело бы смысл, если бы было достаточно большое количество программистов, которые не делают ошибок и делают все правильно с первой попытки. К сожалению, я еще не встречал такого человека. Пока у нас не будет достаточно совершенных программистов, нам нужны инструменты для предотвращения и обнаружения всех ошибок, которые будут происходить. - person Baum mit Augen; 23.05.2015
comment
Баум, дело не в совершенстве. И никогда не было. Вы думаете, что плотники и другие квалифицированные рабочие идеальны? Нет. Но разве они ищут искалеченные инструменты? Опять же, нет. Они работают, чтобы стать лучше. Хорошие программисты делают то же самое. Слабые программисты, которые никогда не станут очень сильными, тяготеют к программному обеспечению. Попросите их написать маршрутизатор для печатных плат на ассемблере... Вряд ли. В то время как хороший программист возьмет руководство по процессору и вручит вам маршрутизатор через несколько дней. И это сработает. Ошибка или две? Возможно. Не конец света в 99,99% случаев. Легко исправляется тоже, скорее всего. - person fyngyrz; 24.05.2015
comment
@fyngyrz Посредственные программисты обычно используют инструменты, которые трудно использовать правильно, а не лучшие инструменты, которые легко использовать, просто потому, что они слишком некомпетентны, чтобы даже представить себе, что их выбор инструментов может быть неоптимальным. - person curiousguy; 20.12.2019

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

#include <iostream>

int main() {
    unsigned n = 42;
    int i = -42;
    if (i < n) {
        std::cout << "All is well\n";
    } else {
        std::cout << "ARITHMETIC IS BROKEN!\n";
    }
}

Правила продвижения означают, что i преобразуется в unsigned для сравнения, что дает большое положительное число и неожиданный результат.

person Mike Seymour    schedule 22.05.2015
comment
Любая причина для понижения? Я хотел бы исправить ответ, если он неправильный. - person Mike Seymour; 22.05.2015
comment
Не понизил голос, а просто предположил: если ваш компилятор позволяет вам это сделать, значит, вы компилируете слишком мало предупреждающих флагов. - person example; 22.05.2015
comment
@example - ваш компилятор должен позволить вам сделать это; код хорошо сформирован, и его значение хорошо определено. Конечно, предупреждение может помочь обнаружить логическую ошибку, но это не главная обязанность компилятора. - person Pete Becker; 23.05.2015
comment
Результат можно сделать более интересным, выполнив сравнения между unsigned n=2; int i=-1, j=1;. Тогда можно будет заметить, что n < i, i < j и j < n верны. - person supercat; 23.05.2015
comment
Текст должен читаться как C++ IS BROKEN. @PeteBecker говорит, что «его значение хорошо определено»; формально это верно, но определение математически смехотворно. Приведения i к unsigned труднее избежать, если вы получаете целочисленный результат, но для сравнения правильно определить язык тривиально. Даже в COBOL было есть On size error, но C(++) просто дает вам достаточно веревки, чтобы повеситься! На VMS DEC C (не знаю насчет ++) предупреждает о знаковом/беззнаковом сравнении/присваивании, тоже правильно (учитывая ломаный язык), - person PJTraill; 23.05.2015
comment
@PJTraill - в C правило такое же: код действителен и четко определен. Конечно, можно было бы потребовать от компилятора генерировать больше кода для смешанных сравнений, но это противоречит фундаментальному принципу разработки C и C++. Если вам нужен большой и медленный, программируйте на C#. - person Pete Becker; 23.05.2015
comment
@PeteBecker Я знаю, что компилятор должен разрешить это, но если вы не компилируете, например. с -Wall -Wextra -pedantic -Wconversion -Wshadow -Werror (мой личный минимальный список предупреждающих флагов) это действительно ваша вина... (даже без -Werror вы получите предупреждение. только без -Wall вы не получите предупреждение - и это просто зло) - person example; 23.05.2015
comment
@PeteBecker Вы действительно думаете, что код для смешанных сравнений «большой и медленный»? В очень небольшом количестве приложений, где дополнительные инструкции 1 (ветвь на отрицательном) или 2 (тест, ветвь) будут иметь значение, люди могут оптимизировать, правильно печатая. Является ли фундаментальный принцип дизайна «быстрым и неправильным»? - person PJTraill; 23.05.2015
comment
@PJTraill - вы действительно не оказываете ОП услугу, вытягивая эту нить в дыру. - person Pete Becker; 23.05.2015
comment
@PeteBecker: Думаю, в этом вы правы — на этом я остановлюсь. - person PJTraill; 23.05.2015
comment
@PeteBecker Если вам нужен большой и медленный, программируйте на C#. -- Это своего рода непрофессиональный комментарий, который не подобает такому зрелому программисту, как вы, особенно с тем вкладом, который вы внесли в C++. Если кто-то и что-то берёт в нору, так это ты. - person ForeverLearning; 09.06.2015

Хотя это можно рассматривать только как вариант существующих ответов: ссылаясь на "Подписанные и неподписанные типы в интерфейсах ," C++ Report, September 1995 Скотта Мейерса, особенно важно избегать беззнаковых типов в интерфейсах.

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

Приведенный там пример:

template <class T>
  class Array {
  public:
      Array(unsigned int size);
  ...

и возможный экземпляр этого класса

int f(); // f and g are functions that return
int g(); // ints; what they do is unimportant
Array<double> a(f()-g()); // array size is f()-g()

Разница значений, возвращаемых f() и g(), может быть отрицательной по целому ряду причин. Конструктор класса Array получит эту разницу как значение, которое неявно преобразуется в unsigned. Таким образом, как разработчик класса Array, вы не можете отличить ошибочно переданное значение -1 от выделения очень большого массива.

person Marco13    schedule 22.05.2015
comment
Разве тот же аргумент не будет справедлив для ссылок или значений? Ясно, что кто-то может ошибочно передать нулевой указатель на Array<double>(*ptrToSize). - person josefx; 25.05.2015
comment
@josefx: Вы можете проверить это. assert(ptr != nullptr) может быть достаточно. Что-то вроде assert(size < theSizeThatIsLikelyToBeAllocated) не работает. Конечно, можно по-прежнему неправильно использовать API со знаковыми типами. Это просто сложнее, и можно покрыть наиболее вероятные ошибки (вызванные такими вещами, как неявные преобразования). - person Marco13; 25.05.2015

Большая проблема с unsigned int заключается в том, что если вы вычтете 1 из unsigned int 0, результат не будет отрицательным числом, результат не меньше, чем число, с которого вы начали, но результатом будет максимально возможное значение unsigned int. .

unsigned int x = 0;
unsigned int y = x - 1;

if (y > x) printf ("What a surprise! \n");

И это то, что делает unsigned int подверженным ошибкам. Конечно, unsigned int работает именно так, как задумано. Это абсолютно безопасно, если вы знаете, что делаете, и не делаете ошибок. Но большинство людей совершают ошибки.

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

person gnasher729    schedule 22.05.2015
comment
Еще неприятнее то, что заданные выражения uint32_t x,y,z;, такие как x-y > z, будут иметь очень разные значения в 32-битных и 64-битных системах. - person supercat; 23.05.2015
comment
@supercat на самом деле будет иметь тот же результат на системах LP32, LP64 и LLP64. Отличаться будут только системы ILP64. - person plugwash; 01.03.2018
comment
@plugwash: я должен был уточнить - в системах, где int - это 64 бита. ИМХО, стандарт выиграл бы от определения не продвигающих типов, поведение которых было бы согласованным для всех компиляторов, которые принимают код, использующий их. Операции, использующие wrap32_t, должны либо давать результат этого типа, когда это возможно, либо вообще отказываться от компиляции (например, потому что компилятор не поддерживает требуемую семантику, или потому что, например, код пытается сложить вместе wrap16_t и wrap32_t — действие, которое не может дать результат, удовлетворяющий обоим ограничениям). - person supercat; 01.03.2018

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

  1. Типы без знака меньше int (например, uint8) содержат числа в диапазоне 0..2ⁿ-1, и вычисления с ними будут вестись в соответствии с правилами целочисленной арифметики при условии, что они не превышают диапазон тип int. Согласно существующим правилам, если такое вычисление выходит за пределы диапазона int, компилятору разрешается делать с кодом все, что ему заблагорассудится, вплоть до отрицания законов времени и причинно-следственных связей (некоторые компиляторы делают именно это!) , и даже если результат вычисления будет присвоен обратно беззнаковому типу, меньшему, чем int.
  2. Типы без знака unsigned int и более крупные содержат элементы абстрактного обертывающего алгебраического кольца целых чисел, конгруэнтных по модулю 2ⁿ; это фактически означает, что если вычисление выходит за пределы диапазона 0..2ⁿ-1, система добавит или вычтет любое число, кратное 2ⁿ, которое потребуется, чтобы вернуть значение в диапазон.

Следовательно, при заданном uint32_t x=1, y=2; выражение x-y может иметь одно из двух значений в зависимости от того, превышает ли int 32 бита.

  1. Если int больше 32 бит, выражение вычтет число 2 из числа 1, что даст число -1. Обратите внимание, что хотя переменная типа uint32_t не может содержать значение -1 независимо от размера int, и сохранение -1 приведет к тому, что такая переменная будет содержать 0xFFFFFFFF, но до тех пор, пока значение не будет приведено к беззнаковому типу, оно будет вести себя как подписанная величина -1.
  2. Если int составляет 32 бита или меньше, выражение даст значение uint32_t, которое при добавлении к значению uint32_t 2 даст значение uint32_t 1 (т. е. значение uint32_t 0xFFFFFFFF).

ИМХО, эту проблему можно было бы решить чисто, если бы C и C++ определяли новые беззнаковые типы [например, unum32_t и uwrap32_t], так что unum32_t всегда будет вести себя как число, независимо от размера int (возможно, потребуется правая операция вычитания или унарного минуса для повышения до следующего большего знакового типа, если int равно 32 битам или меньше), в то время как wrap32_t всегда ведет себя как член алгебраического кольца (блокируя продвижение, даже если int больше 32 бит). Однако в отсутствие таких типов часто невозможно написать код, который одновременно переносим и чист, поскольку переносимый код часто требует приведения типов повсюду.

person supercat    schedule 22.05.2015
comment
Совершенно запутанный ответ. Вы говорите, что правила упаковки и продвижения для целых чисел без знака зависят от их размера, а также от размера базы int? - person Martin Ba; 22.05.2015
comment
@MartinBa: да, это то, что он говорит. Поскольку вы это поняли, я думаю, это не сбивает с толку, но для некоторых это может быть неожиданно :-) Целочисленные типы меньше int являются полным PITA, особенно беззнаковые. - person Steve Jessop; 22.05.2015
comment
@MartinBa: ответ сбивает с толку, потому что основные правила таковы. Я добавил немного больше к первой паре пунктов; помогает ли это. - person supercat; 22.05.2015
comment
@SteveJessop: Согласны ли вы с моей характеристикой uint32_t как одного из двух очень разных типов в зависимости от размера int? Я бы не назвал целочисленные типы меньше int PITA в тех случаях, когда ожидается, что они будут вести себя как числа в допустимом диапазоне. Что проблематично, так это типы, которые меньше int на одних платформах, но не на других; Единственное преимущество, которое я вижу в изменении поведения в зависимости от размера int, заключается в том, что он позволяет существующему коду, написанному для определенных размеров int, работать в системах, где размер int соответствует ожидаемому. - person supercat; 22.05.2015
comment
ваше здоровье. теперь яснее. вторая пуля 1 все еще странная: это uint, как вы можете получить -1 ?? - person Martin Ba; 23.05.2015
comment
@MartinBa: беззнаковые типы меньше int будут повышаться до подписанных int всякий раз, когда над ними выполняются какие-либо вычисления. На обычных 32-битных машинах это наиболее заметно для типов uint8_t и uint16_t. Повышение до int часто полезно, когда значения без знака представляют количества, но может быть катастрофическим в случаях, когда они представляют вещи, которые должны быть обернуты. Обратите внимание, что с учетом uint16_t x=65533; x*=x; компилятор для системы, где unsigned int составляет 16 бит или больше 32 бит, должен установить x=9, но в системе, где unsigned составляет от 17 до 32 бит... - person supercat; 23.05.2015
comment
... Стандарт позволяет компиляторам вместо этого отрицать законы времени и причинности (числовой продукт равен 4294574089; деление на 65536 дает 65530 остатка 9, поэтому выполнение вычисления как uint16_t или выполнение его в типе int, достаточно большом, чтобы вместить 4294574089 и сохранение в uint16_t даст 9). Однако если значение будет преобразовано в тип int, который не может содержать 4294574089, последствия переполнения могут выйти далеко за рамки значения x. - person supercat; 23.05.2015
comment
Ваше здоровье. Мое личное мнение по этому поводу - попытаться убедиться, что также есть обертка подписанных целых чисел - я думаю, что MSVC все равно имеет это, и у gcc есть переключатель для него, поэтому, даже если стандарт называет это как UB, все (?) реализации кажутся предложить способ его определения. - person Martin Ba; 23.05.2015
comment
@MartinBa: Хотя вы правы в том, что почти все реализации на практике предлагают опцию переноса подписанного целого числа, есть пара недостатков: (1) нет стандартных средств, с помощью которых программа на C может запросить такую ​​семантику или отказаться от компиляции, если компилятор не может их предоставить; (2) Требование о переносе целочисленных значений (со знаком или без знака) исключает многие оптимизации, которые часто бывают полезными (хотя иногда и катастрофическими). Мне бы очень хотелось, чтобы C предлагал множество различных типов целых чисел с различной семантикой, выбранной для предоставления многих хороших возможностей оптимизации... - person supercat; 23.05.2015
comment
...но минимум сюрпризов. Чем большую гибкость компиляторы предлагают программистам в отношении того, что их волнует, тем больше возможностей программисты могут предоставить для агрессивной оптимизации вещей, которые не повлияют на правильность. - person supercat; 23.05.2015

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

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

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

В качестве другого примера рассмотрим умножение двух целых чисел без знака одинакового размера.

  • Если размер операнда больше или равен размеру int, тогда умножение будет иметь определенную циклическую семантику.
  • Если размер операнда меньше, чем int, но больше или равен половине размера int, то существует вероятность неопределенного поведения.
  • Если размер операнда меньше половины размера int, то умножение даст численно правильные результаты. Присвоение этого результата обратно переменной исходного беззнакового типа создаст определенную семантику циклического переноса.
person plugwash    schedule 22.05.2017

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

person PRu    schedule 22.05.2015
comment
Не могли бы вы уточнить, какие серьезные проблемы с производительностью, и привести пример кода? - person user694733; 22.05.2015
comment
Если вы преобразуете unsigned в int или наоборот, двоичные представления идентифицируются точно. Таким образом, нет накладных расходов для ЦП, когда вы выполняете преобразование одного в другое. - person example; 22.05.2015
comment
(при условии, что реализация C++ использует представление дополнения до двух для отрицательных целых чисел) - person Ruslan; 22.05.2015
comment
@example бинарный макет не тот. Значение без знака занимает все пространство битов (8,16,32,64), но знаковое значение имеет старший бит для знака, что уменьшает пространство значения на 1 бит. В случае с SIMD-инструкциями нет никого, кто бы выполнял вычисления для обоих типов в одной инструкции. Происходит преобразование с насыщением, то есть падение производительности. - person PRu; 22.05.2015