Дали сравняването на цяло число без знак с -1 е добре дефинирано?

Помислете за следното:

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

Сравнението, чийто резултат инициализира result, има ли добре дефинирано поведение?
И резултатът му true ли е, както бих очаквал?


Тези въпроси и отговори бяха написани, защото не бях сигурен по-специално за два фактора.
И двата могат да бъдат идентифицирани чрез използването на термина „решаващо[ли]“ в моя отговор.

Този пример е вдъхновен от подход за условия на цикъл, когато броячът е без знак:
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
Отне ми малко време да си спомня какво означава underflow (когато число е твърде малко, за да се съхранява като различно от нула в тип с плаваща запетая). Предполагам, че по-добрият термин за това може да е отрицателно преливане.   -  person mwfearnley    schedule 10.12.2014
comment
@mwfearnley: Може би, освен че това вероятно е още по-лошо, тъй като стандартът отделя време (в бележка), за да обясни, че неподписаното препълване е невъзможно. Разбира се, по същата логика unsigned underflow е невъзможно, но поне не го казва изрично. ;) Да, нищо ми няма.   -  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. Тъй като подписан 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);

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 бита.

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, но времето за изпълнение за изпълнение на семантично счупен код е без значение. Ако кодът трябва да има изчислен междинен резултат mod 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
@n.m. -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
Libs/headers на трети страни рядко са голям проблем на практика. Поправям ги, обвивам ги, подавам доклади за грешки, така че да бъдат коригирани, или използвам -isystem (мръсен gcc хак). - person n. 1.8e9-where's-my-share m.; 10.12.2014