Сравнивает ли недопустимое целое число без знака с -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--)
Ответы
Ответ 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"