Сравнивает ли недопустимое целое число без знака с -1 хорошо определенным?
Рассмотрим следующее †:
size_t r = 0;
r--;
const bool result = (r == -1);
Есть ли сравнение, чей результат инициализируется result
иметь четко определенное поведение?
И это его результат true
как я ожидала?
Этот Q&A был написан, потому что я не был уверен в двух факторах, в частности.
Оба могут быть идентифицированы с использованием термина "критический [ly]" в моем ответе.
† Этот пример вдохновлен подходом к условиям цикла, когда счетчик не подписан:
for (size_t r = m.size() - 1; r != -1; r--)
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
, С момента подписания 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);
Стандартные ссылки на C++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 бита, но ни один объект не может быть больше, чем 232-1 байт, так size_t
32 бита.
Да, и результат - то, что вы ожидаете.
Давайте разберемся с этим.
Какова стоимость 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 - число битов, используемых для представления типа без знака). [Примечание: в представлении дополнения до двух это преобразование является концептуальным, и в битовой комбинации нет изменений (если нет усечения). —Конечная записка]
[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"