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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

Ответ 3

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

#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 для сравнения, давая большое положительное число и удивительный результат.

Ответ 4

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

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

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

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 и очень большое распределение массива.

Ответ 5

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

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

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

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

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

Ответ 6

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

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

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

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

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

Ответ 7

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

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

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

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

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

Ответ 8

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