Правильно ли определено сравнение целого числа без знака с занижением до -1?

Рассмотрим следующее:

size_t r = 0;
r--;
const bool result = (r == -1);

Имеет ли сравнение, результат которого инициализирует result, четко определенное поведение?
И является ли его результат true, как я и ожидал?


Эти вопросы и ответы были написаны, потому что я не был уверен в двух особенностях.
Оба они могут быть идентифицированы по использованию термина "важный[ly]" в моем ответе.

Этот пример вдохновлен подходом к условиям цикла, когда счетчик не имеет знака:
for (size_t r = m.size() - 1; r != -1; r--)


person Lightness Races in Orbit    schedule 10.12.2014    source источник
comment
Еще одно замечание: строго говоря, поведение r-- не является недостаточным переполнением, хотя неформально его можно так называть.   -  person Keith Thompson    schedule 10.12.2014
comment
Мне потребовалось некоторое время, чтобы вспомнить, что означает потеря значимости (когда число слишком мало, чтобы хранить его как ненулевое в типе с плавающей запятой). Я полагаю, что лучшим термином для этого может быть отрицательное переполнение.   -  person mwfearnley    schedule 10.12.2014
comment
@mwfearnley: Возможно, за исключением того, что это, возможно, еще хуже, поскольку стандарту требуется время (в примечании), чтобы объяснить, что беззнаковое переполнение невозможно. Конечно, по той же логике невозврат без знака невозможен, но, по крайней мере, об этом прямо не говорится. ;) Да у меня ничего нет.   -  person Lightness Races in Orbit    schedule 11.12.2014
comment
Зависит от реализации. Машина может быть комплиментом.   -  person Joshua    schedule 11.12.2014
comment
Ну, я всегда использую: const bool result = (r == (size_t)-1);. Просто чтобы убедиться, что компилятор интерпретирует -1 как size_t, а не r как какую-то другую переменную типа. Но ваш вариант тоже может быть правильным, просто нужно взглянуть на стандарт.   -  person ST3    schedule 11.12.2014
comment
@ST3: Вы имеете в виду, как это уже сделали оба ответчика 18 часов назад? :)   -  person Lightness Races in Orbit    schedule 11.12.2014


Ответы (2)


size_t r = 0;
r--;
const bool result = (r == -1);

Строго говоря, значение result определяется реализацией. На практике почти наверняка будет true; Я был бы удивлен, если бы была реализация, в которой это false.

Значение r после r-- является значением SIZE_MAX, макроса, определенного в <stddef.h>/<cstddef>.

Для сравнения r == -1 над обоими операндами выполняются обычные арифметические преобразования. Первым шагом обычных арифметических преобразований является применение интегральных преобразований к обоим операндам.

r относится к типу size_t, определяемому реализацией целочисленному типу без знака. -1 — это выражение типа int.

В большинстве систем ширина size_t не меньше int. В таких системах интегральные повышения заставляют значение r либо преобразовываться в unsigned int, либо сохранять свой существующий тип (первое может произойти, если size_t имеет ту же ширину, что и int, но более низкий ранг преобразования). Теперь левый операнд (беззнаковый) имеет по крайней мере ранг правого операнда (со знаком). Правый операнд преобразуется в тип левого операнда. Это преобразование дает то же значение, что и r, поэтому сравнение на равенство дает true.

Это "нормальный" случай.

Предположим, у нас есть реализация, в которой size_t составляет 16 бит (скажем, это typedef для unsigned short), а int — 32 бита. Итак, SIZE_MAX == 65535 и INT_MAX == 2147483647. Или у нас может быть 32-битный size_t и 64-битный int. Сомневаюсь, что такая реализация существует, но стандарт ничего не запрещает (см. ниже).

Теперь левая часть сравнения имеет тип size_t и значение 65535. Поскольку signed int может представлять все значения типа size_t, интегральные повышения преобразуют значение в 65535 типа int. Обе части оператора == имеют тип int, поэтому обычные арифметические преобразования здесь не при чем. Выражение эквивалентно 65535 == -1, что явно равно false.

Как я уже упоминал, подобные вещи вряд ли произойдут с выражением типа size_t, но это легко может произойти с более узкими беззнаковыми типами. Например, если r объявлен как unsigned short, или unsigned char, или даже просто char в системе, где этот тип подписан, значение result, вероятно, будет false. (Я говорю вероятно, потому что short или даже unsigned char могут иметь ту же ширину, что и int, и в этом случае result будет true.)

На практике вы можете избежать потенциальной проблемы, выполнив преобразование явно, а не полагаясь на обычные арифметические преобразования, определяемые реализацией:

const bool result = (r == (size_t)-1);

or

const bool result = (r == SIZE_MAX);

Стандартные ссылки С++ 11:

  • 5.10 [expr.eq] Операторы равенства
  • 5.9 [expr.rel] Операторы отношения (указывает, что выполняются обычные арифметические преобразования)
  • 5 [expr] Выражения, параграф 9: Обычные арифметические преобразования
  • 4.5 [конв.пром] Интегральные акции
  • 18.2 [типы поддержки] size_t

18.2 пункты 6-7:

6 Тип size_t — определяемый реализацией целочисленный тип без знака, который достаточно велик, чтобы содержать размер любого объекта в байтах.

7 [ Примечание: Реализациям рекомендуется выбирать типы для ptrdiff_t и size_t, ранги целочисленного преобразования (4.13) которых не выше, чем у signed long int, за исключением случаев, когда требуется больший размер, чтобы содержать все возможные значения. — примечание в конце]

Так что нет запрета делать size_t уже, чем int. Я могу почти правдоподобно представить себе систему, в которой int – это 64 бита, но ни один объект не может быть больше 232-1 байт, поэтому size_t – это 32 бита.

person Keith Thompson    schedule 10.12.2014
comment
Мммм, да почти наверное. - person Lightness Races in Orbit; 10.12.2014
comment
64-битное ALU с 32-битной адресуемой памятью или страничная система памяти с 32-битными страницами? Какой-то специальный чип потоковой обработки? - person Yakk - Adam Nevraumont; 10.12.2014
comment
@Yakk: Конечно - или очаровательно причудливый дизайнер оборудования. - person Keith Thompson; 10.12.2014
comment
@KeithThompson: я действительно хотел бы, чтобы комитет по стандартам C указал средства объявления обертывания неподписанных типов, которые нельзя было бы неявно конвертировать или продвигать в подписанные типы [сумма или произведение, например, wrap32_t и int были бы wrap32_t независимо от того, было ли int 16, 32 или 64 бит]. Такие типы позволили бы писать чистый код, который был бы действительно переносимым, и гарантировать, что любые приведения типов, которые потребуются для переносимости, потребуются, и точка. Без такой языковой особенности... - person supercat; 10.12.2014
comment
... Я не вижу никакого практического способа перенести код, который предполагает, что uint32_t не будет повышен до знакового типа [очень распространенное предположение] в любую систему, где int больше 32 бит. - person supercat; 10.12.2014
comment
@supercat: И/или комитет по стандартам C++ (этот вопрос помечен как C++, а не C). Я согласен, хотя я не думал о том, как это определить. Существует ряд полезных целочисленных типов: size_t, ptrdiff_t, uintN_t и т. д., которые в идеале должны работать вместе четко определенным образом. Раздражает тот факт, что интегральные продвижения так строго определены в терминах int и unsigned int, которые не имеют точного отношения к другим типам. Идея состоит в том, что целочисленная арифметика для типов уже, чем int, может не поддерживаться (что удобно для многих аппаратных средств), но... - person Keith Thompson; 10.12.2014
comment
... Хотелось бы более чистый способ сделать это. - person Keith Thompson; 10.12.2014
comment
@KeithThompson: Учитывая код (для 8- или 16-битного микро) uint16_t new_reading,last_reading; uint32_t total_consumption;, если оператор total_consumption += new_reading-last_reading; оценивает промежуточный результат как uint16_t, на некоторых платформах будет дороже, чем его оценка как uint32_t, но время выполнения выполнение семантически некорректного кода не имеет значения. Если код должен иметь промежуточный результат, вычисленный по модулю 65536, компилятор должен добавить любые дополнительные инструкции, необходимые для этого. - person supercat; 11.12.2014
comment
Раньше существовала эта машина: ((size_t)0 - 1) == 0xFFFF, -1 == 0xFFFE. - person Joshua; 11.12.2014
comment
@Joshua: 16-битное size_t, представление с дополнением до единицы для целых чисел со знаком, верно? - person Keith Thompson; 11.12.2014
comment
@Joshua Независимо от базового представления для целых чисел со знаком преобразование в беззнаковое всегда выполняется по модулю 2 ^ n. - person T.C.; 11.12.2014

Да, и результат такой, какой вы ожидаете.

Давайте сломаем это.

Каково значение r в этот момент? Что ж, потеря значимости четко определена и приводит к тому, что r принимает свое максимальное значение к моменту запуска сравнения. std::size_t не имеет конкретных известных границ, но мы можем сделать разумные предположения о его диапазоне по сравнению с диапазоном int:

std::size_t — это беззнаковый целочисленный тип результата оператора sizeof. [..] std::size_t может хранить максимальный размер теоретически возможного объекта любого типа (включая массив).

И, просто чтобы не мешать, выражение -1 является унарным -, применяемым к литералу 1, и имеет тип int в любой системе:

[C++11: 2.14.2/2]: Тип целочисленного литерала является первым из соответствующего списка в таблице 6, в котором может быть представлено его значение. [..]

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

Более чем разумно предположить, что в большинстве систем int не сможет содержать std::numeric_limits<std::size_t>::max().

Что происходит с этими операндами?

[C++11: 5.10/1]: Операторы == (равно) и != (не равно) имеют те же семантические ограничения, преобразования и тип результата, что и операторы отношения, за исключением их более низкого приоритета и истинностного результата. [..]

[C++11: 5.9/2]: Обычные арифметические преобразования выполняются над операндами арифметического или перечислительного типа. [..]

Давайте рассмотрим эти «обычные арифметические преобразования»:

[C++11: 5/9]: Многие бинарные операторы, которые ожидают операнды арифметического или перечислительного типа, вызывают преобразования и возвращают типы результатов аналогичным образом. Цель состоит в том, чтобы получить общий тип, который также является типом результата.

Этот шаблон называется обычными арифметическими преобразованиями, которые определяются следующим образом:

  • Если любой из операндов относится к типу перечисления с областью действия (7.2), преобразования не выполняются; если другой операнд не имеет того же типа, выражение имеет неправильный формат.
  • Если один из операндов имеет тип long double, другой должен быть преобразован в long double`.
  • В противном случае, если один из операндов равен double, другой должен быть преобразован в double.
  • В противном случае, если один из операндов равен float, другой должен быть преобразован в float.
  • Otherwise, the integral promotions (4.5) shall be performed on both operands.59 Then the following rules shall be applied to the promoted operands:
    • If both operands have the same type, no further conversion is needed.
    • В противном случае, если оба операнда имеют целые типы со знаком или оба имеют целые типы без знака, операнд с типом меньшего целочисленного ранга преобразования должен быть преобразован в тип операнда с большим рангом.
    • В противном случае, если операнд, имеющий целочисленный тип без знака, имеет ранг, больший или равный рангу типа другого операнда, операнд с целочисленным типом со знаком должен быть преобразован в тип операнда с целочисленным типом без знака.
    • В противном случае, если тип операнда с целочисленным типом со знаком может представлять все значения типа операнда с целочисленным типом без знака, операнд с целочисленным типом без знака должен быть преобразован в тип операнда с целочисленным типом со знаком.
    • В противном случае оба операнда должны быть преобразованы в целочисленный тип без знака, соответствующий типу операнда с целочисленным типом со знаком.

Я выделил отрывок, который вступает в силу здесь, и почему:

[C++11: 4.13/1]: Каждый целочисленный тип имеет ранг целочисленного преобразования, определяемый следующим образом.

  • [..]
  • Ранг long long int должен быть выше ранга long int, который должен быть выше ранга int, который должен быть выше ранга short int, который должен быть выше ранга signed char.
  • Ранг любого целочисленного типа без знака должен быть равен рангу соответствующего целочисленного типа со знаком.
  • [..]

Все целочисленные типы, даже с фиксированной шириной, состоят из стандартных интегральных типов; следовательно, логически std::size_t должно быть unsigned long long, unsigned long или unsigned int.

  • Если std::size_t равно unsigned long long или unsigned long, то ранг std::size_t выше ранга unsigned int и, следовательно, ранга int.

  • Если std::size_t равно unsigned int, ранг std::size_t равен рангу unsigned int и, следовательно, также int.

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

[C++11: 4.7/2]: Если целевой тип беззнаковый, результирующее значение является наименьшим целым числом без знака, соответствующим исходному целому (по модулю 2n, где n — количество битов, используемых для представления беззнакового типа). [ Примечание: В представлении с дополнением до двух это преобразование является концептуальным, и в битовом шаблоне нет изменений (если есть не является усечением). —конец примечания ]

[C++11: 4.7/3]: Если целевой тип подписан, значение не изменяется, если оно может быть представлено в целевом типе (и ширине битового поля); в противном случае значение определяется реализацией.

Это означает, что std::size_t(-1) эквивалентно std::numeric_limits<std::size_t>::max(); очень важно, чтобы значение n в приведенном выше предложении относилось к количеству битов, используемых для представления типа unsigned, а не исходного типа. В противном случае мы бы сделали std::size_t((unsigned int)-1), что совсем не одно и то же, оно может быть на много порядков меньше желаемого значения!

Действительно, теперь, когда мы знаем, что все конверсии четко определены, мы можем проверить это значение:

std::cout << (std::size_t(-1) == std::numeric_limits<size_t>::max()) << '\n';
// "1"

И, просто чтобы проиллюстрировать мою точку зрения ранее, в моей 64-битной системе:

std::cout << std::is_same<unsigned long, std::size_t>::value << '\n';
std::cout << std::is_same<unsigned long, unsigned int>::value << '\n';
std::cout << std::hex << std::showbase
          << std::size_t(-1) << ' '
          << std::size_t(static_cast<unsigned int>(-1)) << '\n';
// "1"
// "0"
// "0xffffffffffffffff 0xffffffff"
person Lightness Races in Orbit    schedule 10.12.2014
comment
-1 не является литералом. Это литерал 1 с примененным к нему унарным оператором -. (Это все еще тип int.) - person Keith Thompson; 10.12.2014
comment
@KeithThompson: Верно. - person Lightness Races in Orbit; 10.12.2014
comment
Более чем разумно предложить... -- я не уверен, что это соответствует тегу language-lawyer. - person Keith Thompson; 10.12.2014
comment
Что ж, я ожидаю сбоя сборки, потому что -Wall -Werror или аналогичный - единственный способ использовать C++, оставаясь при этом относительно нормальным. - person n. 1.8e9-where's-my-share m.; 10.12.2014
comment
@н.м. -Wall -Wextra -pedantic? Да. -Werror? ммм, нет. - person Lightness Races in Orbit; 10.12.2014
comment
Да, -Ошибка. Ноль предупреждений в моих журналах сборки, потому что, если я решу, что некоторые предупреждения в порядке, я должен проверять каждое из них при каждой сборке, и у меня есть лучшее применение своему обильному свободному времени. - person n. 1.8e9-where's-my-share m.; 10.12.2014
comment
@n.m.: Хорошо, но когда сторонние заголовки вызывают предупреждения, вы застряли. И у меня есть дела поважнее, чем взламывать чужой код, чтобы обойти это. У меня также есть дела поважнее, чем исправлять каждое предупреждение о неиспользуемом параметре в ветке разработки на каждой отдельной итерации, пока я нахожусь на первых этапах создания новой функции. - person Lightness Races in Orbit; 10.12.2014
comment
@LightnessRacesinOrbit, по крайней мере, gcc и clang позволяют нам отключать предупреждения с помощью прагм, поэтому для сторонних заголовков я могу легко отключить конкретное предупреждение, и это действительно то, что я делаю, и это работает хорошо. - person Shafik Yaghmour; 10.12.2014
comment
Сторонние библиотеки/заголовки редко представляют большую проблему на практике. Я исправляю их, оборачиваю их, отправляю отчеты об ошибках, чтобы они были исправлены, или использую -isystem (грязный хак gcc). - person n. 1.8e9-where's-my-share m.; 10.12.2014