Ефективний непідписаний підпис, уникаючи поведінки, визначеної реалізацією


93

Я хочу визначити функцію, яка приймає unsigned intяк аргумент і повертає до аргументу intконгруентний модуль UINT_MAX + 1.

Перша спроба може виглядати так:

int unsigned_to_signed(unsigned n)
{
    return static_cast<int>(n);
}

Але, як відомо будь-якому мовному юристу, приведення з непідписаного на підписане значення, більші за INT_MAX, визначається реалізацією.

Я хочу реалізувати це таким чином, що (а) воно покладається лише на поведінку, передбачену специфікацією; і (b) він компілюється у відсутність операцій на будь-якій сучасній машині та оптимізуючий компілятор.

Що стосується химерних машин ... Якщо немає підписаного int-конгруентного за модулем UINT_MAX + 1 неподписаного int, скажімо, я хочу створити виняток. Якщо їх більше одного (я не впевнений, що це можливо), скажімо, я хочу найбільший.

Добре, друга спроба:

int unsigned_to_signed(unsigned n)
{
    int int_n = static_cast<int>(n);

    if (n == static_cast<unsigned>(int_n))
        return int_n;

    // else do something long and complicated
}

Я не дуже дбаю про ефективність, коли я не перебуваю у типовій системі двох компонентів, оскільки, на мою скромну думку, це малоймовірно. І якщо мій код стане вузьким місцем на всюдисущих системах знакових величин 2050 року, ну, я впевнений, хтось зможе це зрозуміти та оптимізувати тоді.

Тепер ця друга спроба досить близька до того, що я хочу. Незважаючи на те, що приведення до intвизначено реалізацією для деяких входів, повернення до unsignedгарантовано стандартом, щоб зберегти значення за модулем UINT_MAX + 1. Отже, умовна перевіряє саме те, що я хочу, і вона скомпілюється в ніщо в будь-якій системі, з якою я, мабуть, не зіткнусь.

Однак ... Я все ще приймаю участь, intне перевіряючи попередньо, чи буде він викликати поведінку, визначену реалізацією. За деякою гіпотетичною системою в 2050 році вона могла робити хтозна-що. Тож, скажімо, я хочу цього уникнути.

Питання: Як повинна виглядати моя "третя спроба"?

Щоб підбити підсумок, я хочу:

  • Трансляція з беззнакового int на підписаний int
  • Зберегти значення mod UINT_MAX + 1
  • Закликайте лише до стандартної поведінки
  • Складіть компроміси на типовій машині з двома доповненнями з оптимізуючим компілятором

[Оновлення]

Дозвольте навести приклад, щоб показати, чому це не тривіальне питання.

Розглянемо гіпотетичну реалізацію С ++ із такими властивостями:

  • sizeof(int) дорівнює 4
  • sizeof(unsigned) дорівнює 4
  • INT_MAX дорівнює 32767
  • INT_MINдорівнює -2 32 + 32768
  • UINT_MAXдорівнює 2 32 - 1
  • Арифметика по intмодулю 2 32 (в діапазон INT_MINчерез INT_MAX)
  • std::numeric_limits<int>::is_modulo правда
  • Відлиття без знака nint зберігає значення 0 <= n <= 32767 і в іншому випадку дає нуль

У цій гіпотетичній реалізації є рівно одне intзначення, конгруентне (мод UINT_MAX + 1) для кожного unsignedзначення. Тож моє запитання було б чітко визначеним.

Я стверджую, що ця гіпотетична реалізація C ++ повністю відповідає специфікаціям C ++ 98, C ++ 03 та C ++ 11. Зізнаюся, я не запам’ятав кожного слова з усіх ... Але я вважаю, що уважно прочитав відповідні розділи. Отже, якщо ви хочете, щоб я прийняв вашу відповідь, ви або повинні (а) навести специфікацію, яка виключає цю гіпотетичну реалізацію, або (б) правильно її обробити.

Дійсно, правильна відповідь повинна обробляти кожну гіпотетичну реалізацію, дозволену стандартом. Ось що, за визначенням, означає "викликати лише стандартно встановлену поведінку".

До речі, зауважте, що std::numeric_limits<int>::is_moduloтут зовсім марно з багатьох причин. З одного боку, це може бути trueнавіть тоді, коли безпідписані зливки не працюють для великих беззнакових значень. З іншого боку, це може бути trueнавіть у системах доповнення чи знакової величини, якщо арифметика просто модульна за цілим цілим діапазоном. І так далі. Якщо ваша відповідь залежить is_modulo, це неправильно.

[Оновлення 2]

Відповідь hvd мене чомусь навчила: Моя гіпотетична реалізація C ++ для цілих чисел не дозволяється сучасною C. Стандарти C99 та C11 дуже конкретно стосуються представлення підписаних цілих чисел; дійсно, вони дозволяють лише доповнення двох, одиницю доповнення та знакову величину (розділ 6.2.6.2, параграф (2);)

Але C ++ - це не C. Як виявляється, цей факт лежить в основі мого запитання.

Оригінальний стандарт C ++ 98 базувався на набагато старішому C89, який говорить (розділ 3.1.2.5):

Для кожного із підписаних цілих типів існує відповідний (але різний) цілий тип без підпису (позначений ключовим словом unsigned), який використовує однаковий обсяг пам’яті (включаючи інформацію про знак) і має однакові вимоги до вирівнювання. Діапазон невід'ємних значень підписаного цілочисельного типу є піддіапазоном відповідного цілого беззнакового типу, і подання одного і того ж значення в кожному типі однакове.

C89 нічого не говорить про те, щоб мати лише один біт знаку або дозволяти лише два-доповнення / одиниці-доповнення / знакову величину.

Стандарт C ++ 98 прийняв цю мову майже дослівно (розділ 3.9.1, параграф (3)):

Для кожного з підписаних цілих типів існує відповідний (але різний) цілий тип без підпису : " unsigned char", " unsigned short int", " unsigned int" та " unsigned long int", кожен з яких займає однакову кількість сховища та має однакові вимоги до вирівнювання (3.9 ) як відповідний цілий тип зі знаком; тобто кожен підписаний цілий тип має те саме представлення об’єкта, що і відповідний цілий тип без підпису . Діапазон невід'ємних значень цілочисельного типу зі знаком є ​​піддіапазоном відповідного цілого типу без знака, і подання значень кожного відповідного типу із знаком / без знака має бути однаковим.

Стандарт C ++ 03 використовує по суті ідентичну мову, як і C ++ 11.

Жодна стандартна специфікація C ++ не обмежує свої цілі числові представлення підписами жодною специфікацією C, наскільки я можу зрозуміти. І нічого не вимагає жодного знакового біта або чогось подібного. Все, що там сказано, це те, що невід’ємні цілі числа зі знаком мають бути піддіапазоном відповідного беззнаку.

Отже, я знову стверджую, що INT_MAX = 32767 з INT_MIN = -2 32 +32768 дозволено. Якщо ваша відповідь передбачає інше, це неправильно, якщо ви не цитуєте стандарт С ++, який підтверджує мені помилку.


@SteveJessop: Власне, я заявив саме те, що хочу в такому випадку: "Якщо до підписаного int немає підписаного int-конгруентного модуля UINT_MAX + 1, скажімо, я хочу створити виняток." Тобто я хочу "правильний" підписаний int за умови його існування. Якщо він не існує - як це може статися у випадку, наприклад, доповнення бітів або подання одиниць доповнення - я хочу виявити це та обробити це для того конкретного виклику закидання.
Nemo

вибачте, не впевнений, як я це пропустив.
Steve Jessop

До речі, я думаю, що у вашій гіпотетичній хитрому впровадженні intпотрібно щонайменше 33 біти, щоб це представити. Я знаю, що це лише виноска, тому ви можете стверджувати, що це ненормативно, але я думаю, що виноска 49 у C ++ 11 має на меті відповідати дійсності (оскільки це визначення терміна, що використовується в стандарті), і це не суперечить що-небудь прямо вказане в нормативному тексті. Отже, всі від'ємні значення повинні бути представлені бітовою схемою, в якій встановлений найвищий біт, і, отже, ви не можете 2^32 - 32768їх перетворити в 32 біти. Не те, щоб ваш аргумент якось покладався на розмір int.
Steve Jessop

А щодо ваших редагувань у відповіді hvd, я думаю, ви неправильно інтерпретували примітку 49. Ви говорите, що величина знаку заборонена, але це не так. Ви читали це так: "значення, представлені послідовними бітами, є адитивними, починаються з 1 і (множаться на послідовну інтегральну потужність 2, за винятком, можливо, біта з найвищою позицією)". Я вважаю, що це слід читати, "значення, представлені послідовними бітами (адитивні, починаються з 1 і помножуються на послідовну інтегральну потужність 2), за винятком, можливо, біта з найвищим положенням". Тобто, всі ставки відключаються, якщо встановлено високий біт.
Steve Jessop

@SteveJessop: Ваше тлумачення може бути правильним. Якщо так, це виключає мою гіпотетичність ... Але це також відкриває справді величезну кількість можливостей, що робить це питання надзвичайно важким для відповіді. Це насправді для мене виглядає як помилка в специфікації. (Очевидно, комітет C так подумав і закріпив це в C99. Цікаво, чому C ++ 11 не застосував їхній підхід?)
Немо

Відповіді:


70

Розширюючи відповідь користувача71404:

int f(unsigned x)
{
    if (x <= INT_MAX)
        return static_cast<int>(x);

    if (x >= INT_MIN)
        return static_cast<int>(x - INT_MIN) + INT_MIN;

    throw x; // Or whatever else you like
}

Якщо x >= INT_MIN(пам’ятайте про правила просування, INT_MINперетворюється на unsigned), то x - INT_MIN <= INT_MAX, отже, це не матиме переповнення.

Якщо це не очевидно, погляньте на претензію "Якщо x >= -4u, тоді x + 4 <= 3.", І майте на увазі, що INT_MAXце буде дорівнювати принаймні математичному значенню -INT_MIN - 1.

У найпоширеніших системах, де це !(x <= INT_MAX)передбачає x >= INT_MIN, оптимізатор повинен мати можливість (і в моїй системі це можливо) видалити другу перевірку, визначити, що два returnоператори можуть бути скомпільовані в один і той же код, і також видалити першу перевірку. Створений список складання:

__Z1fj:
LFB6:
    .cfi_startproc
    movl    4(%esp), %eax
    ret
    .cfi_endproc

Гіпотетична реалізація у вашому питанні:

  • INT_MAX дорівнює 32767
  • INT_MIN дорівнює -2 32 + 32768

неможливо, тому не потребує особливого розгляду. INT_MINбуде дорівнює або -INT_MAX, або -INT_MAX - 1. Це випливає з подання C цілочисельних типів (6.2.6.2), яке вимагає, nщоб біти були бітами значень, один біт - бітом знаку, і допускає лише одне представлення пастки (не враховуючи подання, які недійсні через біти доповнення), а саме той, який інакше представляв би від’ємний нуль / -INT_MAX - 1. C ++ не допускає жодних цілочисельних подань, що перевищують те, що дозволяє C.

Оновлення : компілятор Microsoft, мабуть, цього не помічаєx > 10іx >= 11тестує те саме. Він лише генерує бажаний код, якщоx >= INT_MINйого замінюютьx > INT_MIN - 1u, який він може виявити як запереченняx <= INT_MAX(на цій платформі).

[Оновлення від запитувача (Немо), детально описуючи наше обговорення нижче]

Зараз я вважаю, що ця відповідь працює у всіх випадках, але зі складних причин. Я, швидше за все, нагороджу за це рішення, але я хочу охопити всі криваві подробиці на випадок, якщо когось це цікавить.

Почнемо з C ++ 11, розділ 18.3.3:

Таблиця 31 описує заголовок <climits>.

...

Зміст такого ж , як заголовок стандартної бібліотеки C <limits.h>.

Тут "Стандарт C" означає C99, специфікація якого суворо обмежує представлення підписаних цілих чисел. Вони схожі на цілі числа без знака, але з одним бітом, призначеним для "знаку", і нулем або більше бітів, призначеними для "заповнення". Біти заповнення не вносять значення у ціле число, а знаковий біт робить внесок лише у вигляді двох-доповнення, одиниць-доповнення або знакової величини.

Оскільки C ++ 11 успадковує <climits>макроси від C99, INT_MIN є або -INT_MAX, або -INT_MAX-1, і код hvd гарантовано працює. (Зверніть увагу, що через відступ INT_MAX може бути набагато меншим, ніж UINT_MAX / 2 ... Але завдяки тому, як працює підписаний-> непідписаний привід, ця відповідь це чудово справляє.)

C ++ 03 / C ++ 98 складніше. Він використовує те саме формулювання для успадкування <climits>від "стандарту C", але тепер "стандарт C" означає C89 / C90.

Всі вони - C ++ 98, C ++ 03, C89 / C90 - мають формулювання, яке я даю у своєму запитанні, але також включають це (C ++ 03, розділ 3.9.1, параграф 7):

Представлення інтегральних типів повинні визначати значення за допомогою чистої двійкової системи числення. (44) [ Приклад : цей міжнародний стандарт дозволяє доповнення 2, доповнення 1 і подання величини зі знаком для інтегральних типів.]

Виноска (44) визначає "чисту двійкову систему числення":

Позиційне представлення для цілих чисел, що використовує двійкові цифри 0 і 1, у яких значення, представлені послідовними бітами, є адитивними, починаються з 1 і помножуються на послідовну інтегральну потужність 2, за винятком, можливо, біта з найвищим положенням.

Цікавим у цьому формулюванні є те, що воно суперечить самому собі, оскільки визначення "чистої двійкової системи числення" не дозволяє подання знаку / величини! Це дозволяє високому біту мати, скажімо, значення -2 n-1 (доповнення двоє) або - (2 n-1 -1) (доповнення one). Але немає значення для високого біта, що приводить до знаку / величини.

У будь-якому випадку, моя "гіпотетична реалізація" не кваліфікується як "чисто бінарна" за цим визначенням, тому вона виключається.

Однак той факт, що старший біт є особливим означає, що ми можемо уявити, що він взагалі вносить будь-яку цінність: невелике позитивне значення, величезне позитивне значення, невелике негативне значення або величезне негативне значення. (Якщо біт знаку може сприяти - (2 n-1 -1), чому ні - (2 n-1 -2)? Тощо)

Отже, уявімо собі цілочисельне подання зі знаком, яке присвоює дивне значення біту «знак».

Невелике позитивне значення для знакового біта призведе до позитивного діапазону для int(можливо, такого великого, як unsigned), а код hvd це чудово обробляє.

Величезне позитивне значення для знакового біта призвело б до intтого, що максимум більше, ніж unsignedце заборонено.

Величезне негативне значення для знакового біта призведе до intпредставлення несуміжного діапазону значень та інших формулювань у специфікаційних правилах.

Нарешті, як щодо знакового біта, який вносить невелику негативну величину? Чи могли б ми мати 1 у "знаковому біті", внесіть, скажімо, -37 у значення int? Тоді тоді INT_MAX буде (скажімо) 2 31 -1, а INT_MIN буде -37?

Це призведе до того, що деякі числа матимуть два подання ... Але one-complement дає два подання до нуля, і це дозволено згідно з "Прикладом". Ніде не сказано, що нуль - це єдине ціле число, яке може мати два уявлення. Тому я думаю, що це нове гіпотетичне дозволено специфікацією.

Дійсно, будь-яке від'ємне значення від -1 до, -INT_MAX-1здається, є допустимим як значення для "знакового біта", але нічого менше (щоб діапазон не був суміжним). Іншими словами, INT_MINможе бути що завгодно від -INT_MAX-1до -1.

А тепер, вгадайте що? Щоб другий привід у коді hvd уникав поведінки, визначеної реалізацією, нам просто потрібно x - (unsigned)INT_MINменше або дорівнює INT_MAX. Ми щойно показали INT_MINце принаймні -INT_MAX-1. Очевидно, що xце максимум UINT_MAX. Передача від’ємного числа в знак без знака - те саме, що додавання UINT_MAX+1. Складіть все це разом:

x - (unsigned)INT_MIN <= INT_MAX

якщо і тільки якщо

UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1

Це останнє - те, що ми щойно показали, тому навіть у цьому збоченому випадку код насправді працює.

Це вичерпує всі можливості, таким чином закінчуючи цю надзвичайно академічну вправу.

Підсумок: У C89 / C90 є деяка недостатньо визначена поведінка для підписаних цілих чисел, які успадкували C ++ 98 / C ++ 03. Це виправлено в C99, а C ++ 11 побічно успадковує виправлення, включаючи <limits.h>з C99. Але навіть C ++ 11 зберігає суперечливе формулювання "чистого двійкового подання" ...


Питання оновлено. Я голосую за цю відповідь (поки що), щоб знеохотити інших ... Я позбавлю голосу згодом, бо відповідь цікава. (Правильно для C, але неправильно для C ++. Я думаю.)
Немо

@Nemo У цьому випадку стандарт C застосовується до C ++; по крайней мере, значення в <limits.h>визначені в стандарті C ++ як такі, що таке ж значення , як і в стандарті C, тому всі вимоги Сі для INT_MINі INT_MAXуспадковуються в C ++. Ви правильно зазначили, що C ++ 03 відноситься до C90, а C90 неясний щодо дозволених цілочисельних подань, але зміна C99 (успадкована принаймні через <limits.h>C ++ 11, сподіваємось, і більш простим способом) обмежує його до ці три кодифікували існуючу практику: ніяких інших реалізацій не існувало.

Я згоден з тим, що значення INT_MINтощо успадковується від C. Але це не означає, що значення є. (Дійсно, як вони могли, оскільки кожна реалізація відрізняється?) Ваш висновок, що INT_MINзнаходиться в межах 1, -INT_MAXзалежить від формулювання, яке просто не відображається в жодній специфікації C ++. Отже, хоча C ++ успадковує семантичне значення макросів, специфікація не надає (або успадковує) формулювання, яке підтримує ваші умовиводи. Здається, це недогляд у специфікації C ++, який запобігає повністю відповідний ефективний акторський склад без підпису.
Немо

@Nemo Якщо ви (можливо, правильно) стверджуєте, що C ++ дозволяє інші подання, то для такої реалізації я стверджую, що INT_MIN не потрібно бути мінімальним представленим значенням типу int, оскільки, що стосується C, якщо тип не відповідає Відповідаючи вимогам intстандарту С, він не може будь-яким чином охоплювати цю реалізацію, а стандарт С ++ не надає жодного його визначення, крім "того, що говорить стандарт С". Я перевірю, чи є більш чітке пояснення.

7
Це чудово. Не уявляю, як я пропустив це питання на той час.
Гонки легкості на орбіті

17

Цей код покладається лише на поведінку, передбачену специфікацією, тому вимога (а) легко виконується:

int unsigned_to_signed(unsigned n)
{
  int result = INT_MAX;

  if (n > INT_MAX && n < INT_MIN)
    throw runtime_error("no signed int for this number");

  for (unsigned i = INT_MAX; i != n; --i)
    --result;

  return result;
}

Це не так просто з вимогою (b). Це компілюється в no-op з gcc 4.6.3 (-Os, -O2, -O3) та з clang 3.0 (-Os, -O, -O2, -O3). Intel 12.1.0 відмовляється оптимізувати це. І я не маю інформації про Visual C.


1
Добре, це чудово. Я хотів би розділити нагороду 80:20 ... Я підозрюю, що міркування компілятора йдуть: Якщо цикл не завершується, resultпереповнення; ціле переповнення не визначено; тому цикл закінчується; тому i == nпри припиненні; тому resultдорівнює n. Я все ще маю перевагу відповіді hvd (за непатологічну поведінку на менш розумних компіляторах), але це заслуговує на більшу кількість голосів.
Немо

1
Непідписані визначаються за модулем. Цикл також гарантовано припиняється, оскільки nє деяким беззнаковим значенням і в iпідсумку повинен досягти кожного беззнакового значення.
idupree

7

Оригінальна відповідь вирішила проблему лише для unsigned=> int. Що робити, якщо ми хочемо вирішити загальну проблему "деякого типу без підпису" до відповідного типу зі знаком? Крім того, оригінальна відповідь чудово цитувала розділи стандарту та аналізувала деякі кутові випадки, але це насправді не допомогло мені зрозуміти, чому це спрацювало, тому ця відповідь спробує дати міцну концептуальну основу. Ця відповідь спробує допомогти пояснити "чому" і використати сучасні функції C ++ для спрощення коду.

C ++ 20 відповідь

Проблема різко спростилася з P0907: Підписані цілі числа - це доповнення двох і остаточне формулювання P1236, яке було проголосовано у стандарті C ++ 20. Тепер відповідь максимально проста:

template<std::unsigned_integral T>
constexpr auto cast_to_signed_integer(T const value) {
    return static_cast<std::make_signed_t<T>>(value);
}

Це воно. Кінець static_cast(або акторський склад) нарешті гарантовано зробить те, що вам потрібно для цього питання, і те, що багато програмістів вважало, що це завжди робилося.

C ++ 17 відповідь

У C ++ 17 все набагато складніше. Нам доведеться мати справу з трьома можливими цілочисельними поданнями (доповнення двох, доповнення одиниць та знакова величина). Навіть у тому випадку, коли ми знаємо, що це має бути доповнення двох, тому що ми перевірили діапазон можливих значень, перетворення значення поза діапазоном підписаного цілого числа у це підписане ціле число все одно дає нам результат, визначений реалізацією. Ми повинні використовувати трюки, як ми бачили в інших відповідях.

По-перше, ось код, як загально вирішити проблему:

template<typename T, typename = std::enable_if_t<std::is_unsigned_v<T>>>
constexpr auto cast_to_signed_integer(T const value) {
    using result = std::make_signed_t<T>;
    using result_limits = std::numeric_limits<result>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<T>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<result>(value);
    } else {
        using promoted_unsigned = std::conditional_t<sizeof(T) <= sizeof(unsigned), unsigned, T>;
        using promoted_signed = std::make_signed_t<promoted_unsigned>;
        constexpr auto shift_by_window = [](auto x) {
            // static_cast to avoid conversion warning
            return x - static_cast<decltype(x)>(result_limits::max()) - 1;
        };
        return static_cast<result>(
            shift_by_window( // shift values from common range to negative range
                static_cast<promoted_signed>(
                    shift_by_window( // shift large values into common range
                        static_cast<promoted_unsigned>(value) // cast to avoid promotion to int
                    )
                )
            )
        );
    }
}

Це має на кілька більше приводів, ніж прийнята відповідь, і це для того, щоб переконатись, що від вашого компілятора не було попереджень про невідповідність підписаних / непідписаних та належним чином обробляти правила цілочисельного просування.

Спочатку ми маємо особливий випадок для систем, які не є доповненням двох (і, отже, ми повинні обробляти максимально можливе значення спеціально, оскільки воно не має на що зіставити). Після цього ми переходимо до реального алгоритму.

Друга умова верхнього рівня є простою: ми знаємо, що значення менше або дорівнює максимальному значенню, тому воно відповідає типу результату. Третя умова трохи складніша навіть із коментарями, тому деякі приклади, мабуть, допомогли б зрозуміти, навіщо потрібно кожне твердження.

Концептуальна основа: числовий рядок

По-перше, що це за windowконцепція? Розглянемо наступний числовий рядок:

   |   signed   |
<.........................>
          |  unsigned  |

Виявляється, що для цілих чисел доповнення двох можна розділити підмножину числового рядка, до якого може дійти будь-який тип, на три однаково великі категорії:

- => signed only
= => both
+ => unsigned only

<..-------=======+++++++..>

Це можна легко довести, розглянувши подання. Ціле число без знака починається з 0і використовує всі біти, щоб збільшити значення в степенях 2. Підписане ціле число є абсолютно однаковим для всіх бітів, крім знакового біта, який коштує -(2^position)замість 2^position. Це означає, що для всіх n - 1бітів вони представляють однакові значення. Потім цілі числа без знака мають ще один нормальний біт, який подвоює загальну кількість значень (іншими словами, з цим бітом є стільки ж значень, скільки і без нього). Ця ж логіка має місце для підписаних цілих чисел, за винятком того, що всі значення з цим набором бітів є від'ємними.

Два інших законних цілих числа, доповнення та знакова величина, мають однакові значення, як цілі числа доповнення двох, за винятком одного: найбільш негативне значення. С ++ визначає все про цілочисельні типи, за винятком reinterpret_cast(і С ++ 20 std::bit_cast), з точки зору діапазону репрезентабельних значень, а не з точки зору бітового подання. Це означає, що наш аналіз буде проводитись для кожного з цих трьох подань до тих пір, поки ми ніколи не спробуємо створити представлення пастки. Непідписане значення, яке відображатиметься з цим відсутнім значенням, є досить невдалим: те, що знаходиться посередині беззнакових значень. На щастя, наша перша умова перевіряє (під час компіляції), чи існує таке подання, а потім обробляє його спеціально за допомогою перевірки виконання.

Перша умова обробляє випадок, коли ми знаходимось у =розділі, це означає, що ми знаходимося в області перекриття, де значення в одній можуть бути представлені в іншій без змін. shift_by_windowФункції в коді переміщують всі значення вниз за розміром кожним з цих сегментів (ми повинні відняти максимальне значення , а потім відняти 1 , щоб уникнути арифметичних проблем переповнення). Якщо ми перебуваємо за межами цього регіону (ми знаходимося в регіоні), щоб знову мати унікальне відображення.+ регіоні), нам потрібно перейти на один розмір вікна. Це потрапляє в діапазон перекриття, що означає, що ми можемо безпечно перетворити з непідписаного на підписане, оскільки значення не змінюється. Однак ми ще не закінчили, тому що ми відобразили два непідписані значення до кожного підписаного значення. Тому нам потрібно перейти до наступного вікна (-

Тепер, чи дає це нам результат конгруентного моду UINT_MAX + 1, як просимо у питанні? UINT_MAX + 1еквівалентно 2^n, де n- кількість бітів у поданні значень. Значення, яке ми використовуємо для розміру нашого вікна, дорівнює 2^(n - 1)(кінцевий індекс у послідовності значень на один менше, ніж розмір). Ми віднімаємо це значення двічі, це означає, що віднімаємо, 2 * 2^(n - 1)що дорівнює 2^n. Додавання та віднімання x- це відсутність дії в арифметичному моді x, тому ми не вплинули на початкове значення мода 2^n.

Правильна обробка цілочисельних рекламних акцій

Тому що це загальна функція , а не тільки intі unsigned, ми також повинні стосуються з інтегральними правилами просування по службі. Є два, можливо, цікаві випадки: один, у якого shortменше, intі той, що shortмає такий же розмір, як int.

Приклад: shortменше ніжint

Якщо shortменше, ніж int(поширене на сучасних платформах), ми також знаємо, що unsigned shortможе поміститися в an int, а це означає, що будь-які операції над ним дійсно відбуватимуться int, тому ми явно переходимо до промотованого типу, щоб уникнути цього. Наше остаточне твердження є досить абстрактним і стає легшим для розуміння, якщо ми підставляємо реальні значення. Для нашого першого цікавого випадку, не втрачаючи загальності, давайте розглянемо 16-біт shortі 17-біт int(що все ще дозволено за новими правилами, і це просто означало б, що принаймні в одному з цих двох цілих типів є деякі біти доповнення ):

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int17_t>(
            shift_by_window(
                static_cast<uint17_t>(value)
            )
        )
    )
);

Вирішення максимально можливого 16-бітового значення без знака

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return int16_t(
    shift_by_window(
        int17_t(
            shift_by_window(
                uint17_t(65535)
            )
        )
    )
);

Спрощує до

return int16_t(
    int17_t(
        uint17_t(65535) - uint17_t(32767) - 1
    ) -
    int17_t(32767) -
    1
);

Спрощує до

return int16_t(
    int17_t(uint17_t(32767)) -
    int17_t(32767) -
    1
);

Спрощує до

return int16_t(
    int17_t(32767) -
    int17_t(32767) -
    1
);

Спрощує до

return int16_t(-1);

Ми ставимо якомога більший без підпису і повертаємось -1, успіх!

Приклад: shortтакий же розмір, якint

Якщо shortтакий же розмір, як int(нечасто на сучасних платформах), інтегральне правило просування дещо відрізняється. У цьому випадку shortпідвищує до intта unsigned shortпідвищує до unsigned. На щастя, ми явно відбиваємо кожен результат за типом, для якого хочемо провести обчислення, тому в підсумку ми не маємо проблемних рекламних акцій. Не втрачаючи загальності, давайте розглянемо 16-розрядну shortта 16-розрядну версії int:

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int16_t>(
            shift_by_window(
                static_cast<uint16_t>(value)
            )
        )
    )
);

Вирішення максимально можливого 16-бітового значення без знака

auto x = int16_t(
    uint16_t(65535) - uint16_t(32767) - 1
);
return int16_t(
    x - int16_t(32767) - 1
);

Спрощує до

return int16_t(
    int16_t(32767) - int16_t(32767) - 1
);

Спрощує до

return int16_t(-1);

Ми ставимо якомога більший без підпису і повертаємось -1, успіх!

Що робити, якщо я просто дбаю про intі unsignedне хвилюю попередження, як оригінальне питання?

constexpr int cast_to_signed_integer(unsigned const value) {
    using result_limits = std::numeric_limits<int>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<unsigned>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<int>(value);
    } else {
        constexpr int window = result_limits::min();
        return static_cast<int>(value + window) + window;
    }
}

Перегляньте це в прямому ефірі

https://godbolt.org/z/74hY81

Тут ми бачимо, що clang, gcc та icc не генерують коду для castі cast_to_signed_integer_basicat -O2і -O3, а MSVC не генерує коду at /O2, тому рішення є оптимальним.


3

Ви можете прямо сказати компілятору, що ви хочете зробити:

int unsigned_to_signed(unsigned n) {
  if (n > INT_MAX) {
    if (n <= UINT_MAX + INT_MIN) {
      throw "no result";
    }
    return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1);
  } else {
    return static_cast<int>(n);
  }
}

Компілюється з gcc 4.7.2for x86_64-linux( g++ -O -S test.cpp) to

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret

UINT_MAX- це вираз типу unsigned int, і це робить ваш static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1)тип цілим цілим . Однак це повинно бути можливо виправити, і я сподіваюся, що тоді воно все одно буде скомпільоване.

2

Якщо x це наш внесок ...

Якщо x > INT_MAXми хочемо , щоб знайти константу kтаким чином, що 0< x - k*INT_MAX<INT_MAX .

Це легко - unsigned int k = x / INT_MAX; . Тоді нехайunsigned int x2 = x - k*INT_MAX;

Тепер ми можемо x2передатиint безпечно . Дозволяєint x3 = static_cast<int>(x2);

Тепер ми хочемо відняти щось на зразок UINT_MAX - k * INT_MAX + 1від x3, якщоk > 0 .

Тепер у системі доповнення 2s, якщо x > INT_MAXце працює:

unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;

Зауважте, що UINT_MAX+1в C ++ гарантовано нуль, перетворення в int було noop, і ми віднялиk*INT_MAX потім додали його назад на "те саме значення". Отже, прийнятний оптимізатор повинен мати змогу стерти всю цю дурість!

Це залишає проблему x > INT_MAXчи ні. Ну, ми створюємо 2 гілки, одну з x > INT_MAX, а одну без. Той, у кого немає, виконує протоку, яку компілятор оптимізує до петель. Той, у кого ... виконується петля після завершення роботи оптимізатора. Розумний оптимізатор реалізує обидві гілки на одне і те ж і скидає гілку.

Проблеми: якщо UINT_MAXце дійсно велике відносно INT_MAX, вищезазначене може не спрацювати. Я припускаю це k*INT_MAX <= UINT_MAX+1неявно.

Можливо, ми могли б атакувати це за допомогою таких переліків, як:

enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };

які працюють на 2 та 1 на системі доповнення 2s, я вважаю (чи гарантуємо ми, що ця математика буде працювати? Це складно ...), і робимо логіку, засновану на них, яка легко оптимізує на системах доповнення, які не є 2-ма ...

Це також відкриває виняток. Це можливо лише в тому випадку, якщо UINT_MAX набагато більше, ніж (INT_MIN-INT_MAX), тому ви можете помістити свій код винятку в блок if, який якось задає саме це питання, і це не сповільнить роботу традиційної системи.

Я не зовсім впевнений, як побудувати ці константи часу компіляції, щоб правильно з ними боротися.


UINT_MAXне може бути малим відносно INT_MAX, оскільки специфікація гарантує, що кожен позитивний підписаний int може бути представлений як беззнаковий int. Але UINT_MAX+1дорівнює нулю в кожній системі; непідписана арифметика завжди по модулю UINT_MAX+1. І все-таки тут може бути ядро ​​працездатного підходу ...
Немо,

@Nemo Просто слідуючи цій темі, так що пробачте моє потенційно очевидне запитання: чи є ваше твердження " UINT_MAX+1рівним нулю в кожній системі", встановленій у специфікації '03? Якщо так, чи існує якийсь підрозділ, який я повинен шукати? Спасибі.
WhozCraig

@WhozCraig: Розділ 3.9.1, параграф 4: "Беззнакові цілі числа, оголошені беззнаковими, повинні підкорятися законам арифметики за модулем 2 ^ n, де n - кількість бітів у значенні, представленому цим конкретним розміром цілого числа", із приміткою до слова "Це означає, що беззнакова арифметика не переповнюється, оскільки результат, який не може бути представлений результуючим цілим беззнаковим типом, зменшується за модулем на число, яке на одне перевищує найбільше значення, яке може бути представлене отриманим цілим беззнаковим типом." В основному непідписаний вказаний для роботи так, як ви хочете / очікуєте.
Немо

@Nemo Спасибі. дуже цінував.
WhozCraig

1

std::numeric_limits<int>::is_modulo- константа часу компіляції. так що ви можете використовувати його для спеціалізації на шаблонах. проблема вирішена, принаймні, якщо компілятор грає разом із вкладанням.

#include <limits>
#include <stdexcept>
#include <string>

#ifdef TESTING_SF
    bool const testing_sf = true;
#else
    bool const testing_sf = false;
#endif

// C++ "extensions"
namespace cppx {
    using std::runtime_error;
    using std::string;

    inline bool hopefully( bool const c ) { return c; }
    inline bool throw_x( string const& s ) { throw runtime_error( s ); }

}  // namespace cppx

// C++ "portability perversions"
namespace cppp {
    using cppx::hopefully;
    using cppx::throw_x;
    using std::numeric_limits;

    namespace detail {
        template< bool isTwosComplement >
        int signed_from( unsigned const n )
        {
            if( n <= unsigned( numeric_limits<int>::max() ) )
            {
                return static_cast<int>( n );
            }

            unsigned const u_max = unsigned( -1 );
            unsigned const u_half = u_max/2 + 1;

            if( n == u_half )
            {
                throw_x( "signed_from: unsupported value (negative max)" );
            }

            int const i_quarter = static_cast<int>( u_half/2 );
            int const int_n1 = static_cast<int>( n - u_half );
            int const int_n2 = int_n1 - i_quarter;
            int const int_n3 = int_n2 - i_quarter;

            hopefully( n == static_cast<unsigned>( int_n3 ) )
                || throw_x( "signed_from: range error" );

            return int_n3;
        }

        template<>
        inline int signed_from<true>( unsigned const n )
        {
            return static_cast<int>( n );
        }
    }    // namespace detail

    inline int signed_from( unsigned const n )
    {
        bool const is_modulo = numeric_limits< int >::is_modulo;
        return detail::signed_from< is_modulo && !testing_sf >( n );
    }
}    // namespace cppp

#include <iostream>
using namespace std;
int main()
{
    int const x = cppp::signed_from( -42u );
    wcout << x << endl;
}


EDIT : виправлений код, щоб уникнути можливої ​​пастки на немодульних машинах (відомо, що існує лише одна, а саме архаїчно налаштовані версії Unisys Clearpath). Для простоти це робиться, не підтримуючи значення -2 n -1, де n - кількість intбітів значення, на такій машині (тобто на Clearpath). на практиці машина також не підтримує це значення (тобто, із знаком і величиною або поданням комплементу 1).


1

Я думаю, що тип int становить щонайменше два байти, тому INT_MIN та INT_MAX можуть змінюватися на різних платформах.

Фундаментальні типи

≤climits≥ заголовок


Я проклятий використовувати компілятор для 6809, який за замовчуванням налаштований на "-mint8", де int дорівнює 8 бітам :-( (це середовище розробки для Vectrex) long - 2 байти, long long - 4 байти і Я не уявляю, що таке короткий ...
Грем Тоаль

1

Мої гроші на використання memcpy. Будь-який гідний компілятор знає, як його оптимізувати:

#include <stdio.h>
#include <memory.h>
#include <limits.h>

static inline int unsigned_to_signed(unsigned n)
{
    int result;
    memcpy( &result, &n, sizeof(result));
    return result;
}

int main(int argc, const char * argv[])
{
    unsigned int x = UINT_MAX - 1;
    int xx = unsigned_to_signed(x);
    return xx;
}

Для мене (Xcode 8.3.2, Apple LLVM 8.1, -O3), який видає:

_main:                                  ## @main
Lfunc_begin0:
    .loc    1 21 0                  ## /Users/Someone/main.c:21:0
    .cfi_startproc
## BB#0:
    pushq    %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    ##DEBUG_VALUE: main:argc <- %EDI
    ##DEBUG_VALUE: main:argv <- %RSI
Ltmp3:
    ##DEBUG_VALUE: main:x <- 2147483646
    ##DEBUG_VALUE: main:xx <- 2147483646
    .loc    1 24 5 prologue_end     ## /Users/Someone/main.c:24:5
    movl    $-2, %eax
    popq    %rbp
    retq
Ltmp4:
Lfunc_end0:
    .cfi_endproc

1
Це не відповідає на запитання, оскільки двійкове представлення непідписаного стандартом не гарантує відповідності підписаному поданню.
TLW
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.