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

Сравнивает ли недопустимое целое число без знака с -1 четко определенным?

Рассмотрим следующий & dagger;:

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

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


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

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

4b9b3361

Ответ 1

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 бит (скажем, a typedef для unsigned short) и int - 32 бита. Итак, SIZE_MAX == 65535 и INT_MAX == 2147483647. Или у нас может быть 32-разрядный size_t и 64-разрядный int. Я сомневаюсь, что такая реализация существует, но ничто в стандарте не запрещает ее (см. Ниже).

Теперь левая сторона сравнения имеет тип size_t и значение 65535. Так как подписанный 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);

или

const bool result = (r == SIZE_MAX);

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

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

18.2 абзацы 6-7:

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

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

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

Ответ 2

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

Позвольте сломать его.

Каково значение 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.
  • В противном случае интегральные акции (4.5) должны выполняться в обоих операндах. 59 Затем к продвинутым операндам применяются следующие правила:
    • Если оба операнда имеют один и тот же тип, дальнейшее преобразование не требуется.
    • В противном случае, если оба операнда имеют целочисленные типы или оба имеют неподписанные целые типы, операнд с типом младшего целочисленного ранга преобразования преобразуется в тип операнд с большим рангом.
    • В противном случае, если операнд с целым типом без знака имеет ранг, больший или равный рангам типа другого операнда, операнд со знаком целочисленного типа должен быть преобразован в тип операнда с целым типом без знака.
    • В противном случае, если тип операнда со знаком целочисленного типа может представлять все значения типа операнда с целым типом без знака, операнд с целым типом без знака должен быть преобразован в тип операнда со знаком целого числа тип.
    • В противном случае оба операнда должны быть преобразованы в целочисленный тип без знака, соответствующий тип операнда со знаком целочисленного типа.

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

[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]: Если тип назначения не указан, результирующее значение представляет собой наименьшее беззнаковое целое, совпадающее с исходным целым (по модулю 2 n где n - количество бит, используемых для представления unsigned type). [Примечание. В представлении с двумя дополнениями это преобразование является концептуальным, и в битовой схеме нет изменений (если нет усечения). -end note]

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

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