Чи допускає стандарт C ++, щоб неініціалізований bool збів програму?


500

Я знаю, що «невизначена поведінка» в C ++ може в значній мірі дозволити компілятору робити все, що завгодно. Однак у мене стався збій, який мене здивував, оскільки я припускав, що код досить безпечний.

У цьому випадку справжня проблема траплялася лише на певній платформі з використанням конкретного компілятора, і лише за умови активізації оптимізації.

Я спробував кілька речей, щоб відтворити проблему і максимально спростити її. Ось витяг функції Serialize, яка називається , яка б приймала параметр bool і копіювала рядок trueабо falseв існуючий буфер призначення.

Чи була б ця функція в огляді коду, не було б способу сказати, що вона, власне, може зірватися, якщо параметр bool був неініціалізованим?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

Якщо цей код виконується за допомогою оптимізації clang 5.0.0 +, він може / може вийти з ладу.

boolValue ? "true" : "false"Я припускав, що очікуваний термінальний оператор виглядав досить безпечно для мене, я припускав: "Яке б значення сміття boolValueне було, не має значення, оскільки воно так чи інакше буде оцінено справжнім або хибним".

Я встановив приклад провідника компілятора, який показує проблему при розбиранні, ось повний приклад. Примітка: для того, щоб спростити проблему, комбінація, яку я виявив, працювала, використовуючи Clang 5.0.0 з оптимізацією -O2.

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

Проблема виникає через оптимізатора: Досить розумно було вивести, що рядки "true" та "false" відрізняються по довжині лише на 1. Отже, замість того, щоб насправді обчислювати довжину, вона використовує значення самого bool, яке повинно технічно бути 0 або 1, і виходить так:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

Хоча це "розумно", так би мовити, моє запитання таке: чи дозволяє стандарт C ++ компілятору припускати bool, може мати лише внутрішнє числове подання '0' або '1' і використовувати його таким чином?

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


200
Це чудове питання. Це є надійною ілюстрацією того, як невизначена поведінка - це не лише теоретична проблема. Коли люди кажуть, що внаслідок UB може статися що завгодно, це "що завгодно" може бути дуже дивовижним. Можна припустити, що невизначена поведінка все ще проявляється передбачуваними способами, але в наші дні з сучасними оптимізаторами це зовсім не так. ОП знадобило час для створення MCVE, ретельно дослідило проблему, оглянуло розбирання та поставило чітке, прямо зрозуміле запитання щодо цього. Неможливо просити більше.
Джон Кугельман

7
Зауважте, що вимога, яку "ненульовий оцінює true", - це правило про булеві операції, включаючи "присвоєння bool" (яке може неявно викликати a static_cast<bool>()залежно від конкретних особливостей). Однак це не є вимогою щодо внутрішнього представництва boolобраного компілятором.
Euro

2
Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew

3
На дуже спорідненій ноті, це "веселе" джерело бінарної несумісності. Якщо у вас є ABI A, що значення нульових колодок перед тим, як викликати функцію, але компілює функції таким чином, що він передбачає, що параметри є нульовими, а ABI B - навпаки (не нульовий pad, але не передбачає нуля -вкладені параметри), він здебільшого буде працювати, але функція, що використовує B ABI, спричинить проблеми, якщо він викликає функцію за допомогою A ABI, яка приймає параметр 'small'. IIRC у вас є це на x86 з clang та ICC.
TLW

1
@TLW: Хоча Стандарт не вимагає, щоб реалізація забезпечувала будь-які засоби виклику або виклику за допомогою зовнішнього коду, було б корисно мати засоби для визначення таких речей для реалізацій там, де вони є релевантними (реалізації, де таких деталей немає відповідні можуть ігнорувати такі атрибути).
Supercat

Відповіді:


285

Так, ISO C ++ дозволяє (але не вимагає) реалізацій зробити цей вибір.

Але також зауважте, що ISO C ++ дозволяє компілятору видавати код, який спеціально виходить з ладу (наприклад, з незаконною інструкцією), якщо програма стикається з UB, наприклад, як спосіб допомогти вам знайти помилки. (Або тому, що це DeathStation 9000. Будь-яка чітка відповідність недостатня для того, щоб реалізація C ++ була корисною для будь-яких реальних цілей). Таким чином, ISO C ++ дозволив би компілятору зробити asm, який вийшов з ладу (з абсолютно інших причин) навіть на аналогічному коді, який читав неініціалізований uint32_t. Навіть незважаючи на те, що для цього потрібно мати фіксований макет без представлень пасток.

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


Ви компілюєте для x86-64 System V ABI , який вказує, що boolаргумент функції в реєстрі представлений бітовими шаблонами false=0таtrue=1 низькими 8 бітами регістра 1 . У пам’яті bool- це 1-байтовий тип, який знову повинен мати ціле значення 0 або 1.

(ABI - це набір варіантів реалізації, про який погоджуються компілятори для однієї платформи, щоб вони могли робити код, який викликає функції один одного, включаючи розміри типів, правила компонування структури та конвенції викликів.)

ISO C ++ не визначає це, але це рішення ABI є широко поширеним, оскільки робить bool-> int перетворення дешевим (просто нульове розширення) . Мені невідомі жодні ABI, які не дозволяють компілятору вважати 0 або 1 boolдля будь-якої архітектури (не лише x86). Це дозволяє оптимізації , як !myboolз xor eax,1фліп молодший біт: Будь-який можливий код , який може перевернути біт / ціле / логічне значення між 0 і 1 в одній команді процесора . Або компіляція a&&bв бітне І для boolтипів. Деякі компілятори насправді використовують переваги булевих значень як 8 біт у компіляторах. Чи операції над ними неефективні? .

Взагалі правило як-якщо дозволяє компілятору скористатись речами, що відповідають дійсності на цільовій платформі, для якої компілюється , тому що кінцевим результатом буде виконуваний код, який реалізує ту саму зовнішньо видиму поведінку, що і джерело C ++. (З усіма обмеженнями, які Undefined Behavior накладає на те, що насправді є "зовнішнім": не з налагоджувачем, а з іншої нитки у добре сформованій / законній програмі C ++.)

Компілятор, безумовно , дозволило в повній мірі скористався гарантією ABI в його код-ген, і зробити код , як ви знайшли , який оптимізує strlen(whichString)до
5U - boolValue.
(До речі, ця оптимізація є якоюсь розумною, але, можливо, короткозорою проти розгалуження та вбудовування memcpyяк сховища негайних даних 2. )

Або компілятор міг створити таблицю покажчиків і проіндексував її цілим числом значення bool, знову припустивши, що це 0 або 1. ( Ця можливість є відповіддю відповіді @ Barmar .)


Ваш __attribute((noinline))конструктор з увімкненою оптимізацією призвів до того, що кланг просто завантажив байт із стека для використання як uninitializedBool. Це зробило простір для об'єкта в mainс push rax(який менше , так і для різних причин приблизно так ефективно , як sub rsp, 8), так що все , що сміття було в AL на вході , щоб mainце значення, яке використовується для uninitializedBool. Ось чому ви насправді отримали значення, які були не просто 0.

5U - random garbageможе легко перетворитись на велике неподписане значення, що приводить memcpy, щоб увійти в непідписану пам'ять. Місце призначення знаходиться в статичному сховищі, а не в стеці, тому ви не перезаписуєте зворотну адресу чи щось.


Інші реалізації можуть робити різні варіанти, наприклад, false=0та true=any non-zero value. Тоді clang, ймовірно, не зробить код, який виходить з ладу для цього конкретного примірника UB. (Але це все одно буде дозволено, якби це хотілося.) Я не знаю жодної реалізації, яка вибирає щось інше, для чого займається x86-64 bool, але стандарт C ++ дозволяє багато речей, які ніхто не хоче і навіть не хоче робити апаратне забезпечення, що є як би то не було поточних процесорів.

ISO C ++ залишає не визначеним, що ви знайдете під час вивчення або зміни об’єктного поданняbool . (наприклад, використовуючи memcpyвказівку boolв unsigned char, що вам дозволено робити, тому що ви char*можете мати псевдонім будь-що. І unsigned charгарантовано не буде бітів для забивання, тому стандарт C ++ офіційно дозволяє вам безперервно представити об'єкти без будь-якого UB. Вказівник-кастинг для копіювання об'єкта представлення відрізняється від призначення char foo = my_bool, звичайно, тому булеанізація до 0 або 1 не відбудеться, і ви отримаєте необроблене представлення об'єкта.)

Ви частково "сховали" UB на цьому шляху виконання від компілятораnoinline . Навіть якщо вона не вбудована, міжпроцедурна оптимізація все ж може зробити версію функції, яка залежить від визначення іншої функції. (По-перше, clang створює виконуваний файл, а не спільну бібліотеку Unix, де може відбуватися перестановка символів. По-друге, визначення всередині class{}визначення, тому всі одиниці перекладу повинні мати однакове визначення. Як і для inlineключового слова.)

Таким чином, компілятор міг би видати просто retабо ud2(незаконну інструкцію) як визначення для main, оскільки шлях виконання, що починається вгорі, mainнеминуче стикається з невизначеною поведінкою. (Що компілятор може бачити під час компіляції, якщо він вирішив пройти шлях через не вбудований конструктор.)

Будь-яка програма, яка стикається з UB, абсолютно не визначена за все своє існування. Але UB всередині функції або if()гілки, яка ніколи фактично не працює, не пошкоджує решту програми. На практиці це означає, що компілятори можуть вирішити видавати незаконну інструкцію або а retабо нічого не випромінювати і потрапляти в наступний блок / функцію для всього базового блоку, який може бути доведено під час компіляції містити або вести до UB.

GCC і Clang на практиці ж на самому ділі іноді виділяють ud2на УБ, а навіть намагається генерувати код для шляхів виконання , які не мають ніякого сенсу. Або для таких випадків, як відпадання кінця нефункції void, gcc іноді опускає retінструкцію. Якщо ви думали, що "моя функція просто повернеться з будь-яким сміттям у RAX", ви сильно помиляєтесь. Сучасні компілятори C ++ вже не ставляться до цієї мови, як до портативної мови складання. Ваша програма дійсно повинна бути дійсною C ++, не роблячи припущень про те, як може виглядати окрема неінлікована версія вашої функції в зоні.

Ще один цікавий приклад: Чому нестандартний доступ до пам'яті mmap'ed іноді є стандартним на AMD64? . x86 не винна у нерівних цілих числах, правда? То чому б несогласований uint16_t*був проблемою? Тому що alignof(uint16_t) == 2і порушення цього припущення призвело до сегментації під час автоматичної векторизації з SSE2.

Дивіться також те, що повинен знати кожен програміст на C щодо не визначеної поведінки №1 / 3 , статтю розробника кланг.

Ключовий момент: якщо компілятор помітив UB під час компіляції, він може "зламати" (випромінювати дивовижну asm) шлях через ваш код, який викликає UB, навіть якщо націлений на ABI, де будь-який біт-шаблон є дійсним представленням об'єкта bool.

Очікуйте повну неприязнь до багатьох помилок програміста, особливо про те, про що попереджають сучасні компілятори. Ось чому ви повинні використовувати -Wallта виправляти попередження. C ++ не є зручною для користувачів мовою, і щось на C ++ може бути небезпечним, навіть якщо це було б безпечно в Asm для цілі, яку ви збираєте. (наприклад, підписаний переповнення є UB в C ++, і компілятори будуть вважати, що цього не відбувається, навіть якщо компілюєте для 2-го додатка x86, якщо ви не використовуєте clang/gcc -fwrapv.)

Унікальний UB, який бачить час, завжди небезпечний, і важко бути впевненим (оптимізуючи час зв’язку), що ви дійсно приховали UB від компілятора, і, таким чином, можете міркувати про те, який саме ASM він буде генерувати.

Не бути надмірно драматичним; часто компілятори дозволяють вам уникнути якихось речей і видавати код, як ви очікуєте, навіть коли щось є UB. Але, можливо, це буде проблемою у майбутньому, якщо розробники компілятора впровадять певну оптимізацію, яка отримує більше інформації про діапазони значень (наприклад, що змінна є негативною, можливо, дозволяє оптимізувати розширення знаків до вільного нульового розширення на x86- 64). Наприклад, у поточних gcc та clang виконання tmp = a+INT_MINне оптимізується a<0як завжди-false, лише те, що tmpзавжди є негативним. (Оскільки INT_MIN+ a=INT_MAXє негативною для цілі доповнення цього 2, і aне може бути вищою за це.)

Таким чином, gcc / clang в даний час не дає змоги отримати інформацію про діапазон для входів обчислення, лише на основі результатів на основі припущення про відсутність підписаного переповнення: приклад на Godbolt . Я не знаю, чи це оптимізація навмисно "пропущена" в ім'я зручності користувача чи що.

Також зауважте, що реалізаціям (aka компіляторам) дозволяється визначати поведінку, яку ISO C ++ залишає невизначеною . Наприклад, усі компілятори, які підтримують інтелігенцію Intel (як, наприклад, _mm_add_ps(__m128, __m128)ручна векторизація SIMD), повинні дозволяти формувати неправильно вирівняні покажчики, що є UB в C ++, навіть якщо ви їх не відмежуєте. __m128i _mm_loadu_si128(const __m128i *)робить нерівномірні навантаження, приймаючи нерівне __m128i*аргумент, а не void*або char*. Чи є `reinterpret_cast`ing між апаратним векторним вказівником та відповідним типом невизначеною поведінкою?

GNU C / C ++ також визначає поведінку зсуву ліворуч від'ємного підписаного номера (навіть без -fwrapv), окремо від звичайних правил UB-переповнення підписаних даних. ( Це UB в ISO C ++ , тоді як правильні зрушення підписаних чисел визначаються реалізацією (логічне проти арифметичного); реалізації хорошої якості вибирають арифметику на HW, яка має арифметичні праві зрушення, але ISO C ++ не визначає). Це задокументовано в розділі Integer посібника GCC разом із визначенням поведінки, визначеної реалізацією, що стандарти C вимагають від імплементації так чи інакше.

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


Виноска 1 : Верхні 56 біт можуть бути сміттям, яке повинен ігнорувати, як зазвичай, для типів, вужчих за регістр.

( Інші АБІС зробити зробити різні варіанти тут . Деякі з них вимагають цілих вузьких типів бути нульовими або знаковим розширенням для заповнення регістра при передачі або повернулися з функцій, таких як MIPS64 і PowerPC64 см. Останній розділ цього x86-64 відповіді який порівнює порівняно з попередніми МСА .)

Наприклад, абонент, можливо, розраховував a & 0x01010101в RDI і використовував його для чогось іншого, перш ніж викликати bool_func(a&1). Абонент може оптимізувати цю функцію, &1оскільки вона вже зробила це до низького байту and edi, 0x01010101, і знає, що позивач повинен ігнорувати високі байти.

Або якщо bool передається як третій аргумент, можливо, абонент, що оптимізує розмір коду, завантажує його mov dl, [mem]замість movzx edx, [mem], зберігаючи 1 байт за рахунок помилкової залежності від старого значення RDX (або іншого ефекту часткового реєстру, залежно від на моделі процесора). Або для першого аргументу mov dil, byte [r10]замість цього movzx edi, byte [r10], оскільки обом потрібен префікс REX.

Саме тому брязкіт випромінює movzx eax, dilв Serialize, замість sub eax, edi. (Для цілих аргументів, clang порушує це правило ABI, замість цього, залежно від недокументованої поведінки gcc і clang до нуля або знака-розширення вузьких цілих чисел до 32 біт. Потрібно розширення знака або нуля при додаванні зміщення на 32 біт до покажчика для x86-64 ABI? Тому мені було цікаво побачити, що це не робить те саме bool.)


Вимітка 2: Після розгалуження вам просто доведеться 4-байтовий movпроміжний або 4-байтовий 1-байтовий запас. Довжина неявна в ширинах магазину + зміщення.

OTOH, glibc memcpy зробить два 4-байтові навантаження / сховища з перекриттям, що залежить від довжини, так що це дійсно в кінцевому підсумку робить всю річ вільною від умовних гілок на булі. Дивіться L(between_4_7):блок у memcpy / memmove glibc. Або, принаймні, піти тим самим шляхом для будь-якого булевого розгалуження в memcpy, щоб вибрати розмір шматка.

Якщо вбудовано, ви можете використовувати 2x mov-immediate + cmovта умовний зсув, або ви можете залишити рядкові дані в пам'яті.

Або якщо налаштування для Intel Ice Lake ( з функцією Fast Short REP MOV ), фактична rep movsbможе бути оптимальною. glibc memcpyможе почати використовувати rep movsb для невеликих розмірів на процесорах з цією функцією, економлячи багато розгалуження.


Інструменти для виявлення UB та використання неініціалізованих значень

У gcc та clang ви можете компілювати, -fsanitize=undefinedщоб додати інструментарій для виконання часу, який попереджатиме або помилятиметься на UB, що відбувається під час виконання. Однак це не сприймає неітіалізовані змінні. (Оскільки це не збільшує розміри типів, щоб звільнити місце для "неініціалізованого" біта).

Дивіться https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/

Щоб знайти використання неініціалізованих даних, є адреса Sanitizer та Memory Sanitizer у clang / LLVM. https://github.com/google/sanitizers/wiki/MemorySanitizer показано приклади clang -fsanitize=memory -fPIE -pieвиявлення неініціалізованих зчитувань пам'яті. Це може працювати найкраще, якщо ви компілюєте без оптимізації, тому всі зчитування змінних закінчуються фактично завантаженням із пам'яті в ASM. Вони показують, що він використовується -O2в тому випадку, коли навантаження не оптимізується. Я сам цього не пробував. (У деяких випадках, наприклад, не ініціалізуючи акумулятор перед підсумовуванням масиву, clang -O3 буде випромінювати код, який підсумовується у векторному реєстрі, який він ніколи не ініціалізував. Тож за допомогою оптимізації ви можете мати випадок, коли пам'яті, зчитуваної з UB, немає. . Але-fsanitize=memory змінює створений asm і може призвести до перевірки цього.)

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

MemorySanitizer реалізує підмножину функціональних можливостей, знайдених у Valgrind (інструмент Memcheck).

Це повинно працювати в цьому випадку, оскільки виклик glibc memcpyз lengthобчисленою з неініціалізованої пам'яті (всередині бібліотеки) призведе до галузі, заснованої на length. Якби він накреслив повністю безроздільну версію, яка щойно використовується cmov, індексування та два магазини, вона, можливо, не працювала б.

Valgrind'smemcheck також буде шукати подібну проблему, знову не скаржачись, якщо програма просто копіює навколо неініціалізованих даних. Але він говорить, що виявить, коли "Умовний стрибок або переміщення залежить від неініціалізованих значень (ів)", спробувати вловити будь-яку зовнішньо видиму поведінку, що залежить від неініціалізованих даних.

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


2
Я бачив гірший випадок, коли змінна приймала значення не в межах 8-бітного цілого числа, а лише у всьому регістрі процесора. А у Itanium є ще гірший, використання неініціалізованої змінної може вийти з ладу прямо.
Джошуа

2
@Joshua: о так, добре, явна спекуляція Itanium буде мітити регістрові значення з еквівалентом "не число", таким чином, використовуючи значення помилок.
Пітер Кордес

11
Більше того, це також ілюструє, чому в дизайні мов C і C ++ в першу чергу була введена функція UB: тому що вона дає компілятору саме таку свободу, що дозволило найсучаснішим компіляторам виконувати ці високоякісні послуги оптимізації, які роблять C / C ++ такими високоефективними мовами середнього рівня.
The_Sympathizer

2
І тому війна між авторами компіляторів C ++ та програмістами C ++, які намагаються писати корисні програми, триває. Ця відповідь, цілком вичерпна у відповіді на це запитання, також може бути використана, оскільки є переконливою копією реклами для постачальників інструментів статичного аналізу ...
davidbak

4
@The_Sympathizer: UB було включено, щоб дозволити реалізаціям вести себе будь-якими способами, які були б найбільш корисні для їхніх клієнтів . Не передбачалося припускати, що всі форми поведінки слід вважати однаково корисними.
Supercat

56

Компілятору дозволяється припускати, що булеве значення, передане як аргумент, є дійсним булевим значенням (тобто таким, яке було ініціалізовано або перетворено в trueабо false). trueЗначення не повинно бути таким же , як ціле число 1 - в самому справі, може бути різними уявленнями trueі false- але параметр повинен бути яким - то правильним поданням одного з цих двох значень, де «діє представництво» є реалізації- визначений.

Тож якщо ви не зможете ініціалізувати a bool, або якщо вам вдасться перезаписати його через якийсь покажчик іншого типу, тоді припущення компілятора будуть помилковими, і настане Невизначена поведінка. Вас попередили:

50) Використання значення bool способами, описаними цим Міжнародним стандартом як "невизначені", наприклад, шляхом вивчення значення неініціалізованого автоматичного об'єкта, може призвести до того, що воно поводиться так, ніби воно не є ні істинним, ні хибним. (Виноска до пункту 6 пункту 6.9.1, Основні типи)


11
" trueЗначення не повинно бути таким самим, як ціле число 1" є таким чином вводити в оману. Звичайно, фактичний бітовий зразок може бути чимось іншим, але коли імпліцитно перетворюється / просувається (єдиний спосіб, коли ви бачите значення, відмінне від true/ false), trueє завжди 1і falseє завжди0 . Звичайно, такий компілятор також не міг би використати трюк, який цей компілятор намагався використати (використовуючи той факт, що boolфактичний бітовий зразок міг бути лише 0або 1), тому це не має ніякого значення для проблеми ОП.
ShadowRanger

4
@ShadowRanger Ви завжди можете безпосередньо перевірити представлення об'єкта.
ТК

7
@shadowranger: моя думка полягає в тому, що реалізація відповідає. Якщо він обмежує дійсні уявлення про trueбітовий шаблон 1, це його прерогатива. Якщо він обирає якийсь інший набір уявлень, то він справді не міг би використовувати оптимізацію, зазначену тут. Якщо вона все-таки вибере саме це представництво, то може. Їй потрібно лише бути внутрішньо послідовним. Ви можете вивчити подання а bool, скопіювавши його в байтовий масив; це не UB (але це визначено реалізацією)
rici

3
Так, оптимізація компіляторів (тобто реальна реалізація C ++) часто інколи видає код, який залежить від boolнаявності бітового шаблону 0або 1. Вони не повторно завантажуються під boolчас кожного читання з пам'яті (або реєстру, що містить аргумент функції). Ось що говорить ця відповідь. Приклади : gcc4.7 + можна оптимізувати , return a||bщоб or eax, ediу функції повернення bool, або MSVC можна оптимізувати a&bдля test cl, dl. x86's testє побітним and , так що якщо cl=1і dl=2тестові встановити прапори відповідно до cl&dl = 0.
Пітер Кордес

5
Суть щодо невизначеної поведінки полягає в тому, що компілятору дозволено робити набагато більше висновків з цього приводу, наприклад, припустити, що кодовий шлях, який би призвів до доступу до неініціалізованого значення, взагалі ніколи не приймається, оскільки забезпечення того, що саме відповідальність програміста . Тож справа не лише в можливості того, щоб значення низького рівня могли бути різними, ніж нульові або одиничні.
Хольгер

52

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

Помилка знаходиться у функції виклику, і її можна було виявити за допомогою перегляду коду або статичного аналізу функції виклику. Використовуючи посилання на провідник компілятора, компілятор gcc 8.2 виявляє помилку. (Можливо, ви можете подати звіт про помилку до клангу, що він не знаходить проблему).

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

NB. Відповідь "Чи може невизначена поведінка викликати _____?" завжди "Так". Це буквально визначення невизначеної поведінки.


2
Чи вірний перший пункт? Чи просто копіює неініціалізований boolтригер UB?
Джошуа Зелений

10
@JoshuaGreen див. [Dcl.init] / 12 "Якщо за допомогою оцінки визначається невизначене значення, поведінка не визначається, за винятком таких випадків:" (і жоден із цих випадків не має винятку для bool). Копіювання вимагає оцінки джерела
ММ

8
@JoshuaGreen І причиною цього є те, що у вас може бути платформа, яка запускає апаратну помилку, якщо ви отримуєте доступ до деяких недійсних значень для деяких типів. Їх іноді називають "уявленнями про пастки".
Девід Шварц

7
Itanium, хоч і незрозумілий, є процесором, який все ще знаходиться у виробництві, має значення пастки та має принаймні два напівсучасні компілятори C ++ (Intel / HP). Вона в буквальному сенсі є true, falseі not-a-thingзначення для булевих.
MSalters

3
З іншого боку, відповідь "Чи вимагає стандарт, щоб всі компілятори обробляли щось певним чином", як правило, "ні", навіть / особливо у випадках, коли очевидно, що будь-який компілятор якості повинен робити це; чим очевиднішим є щось, тим меншою потребою має бути автори стандарту, щоб насправді це сказати.
Supercat

23

Буль дозволений лише для вмісту залежних від реалізації значень, використовуваних внутрішньо для trueта false, і згенерований код може вважати, що він буде містити лише одне з цих двох значень.

Як правило, реалізація використовуватиме ціле число 0для falseі 1для true, щоб спростити перетворення між boolі int, а також if (boolvar)згенерувати той самий код, що і if (intvar). У такому випадку можна уявити, що код, сформований для потрійника у призначенні, використовував би значення як індекс у масиві покажчиків на два рядки, тобто він може бути перетворений на щось на кшталт:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

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


1
@SidS Дякую Теоретично, внутрішні уявлення можуть бути протилежними тому, як вони переходять до / з цілих чисел, але це було б викривленим.
Бармар

1
Ви маєте рацію, і ваш приклад також зазнає краху. Однак для огляду коду "видно", що ви використовуєте неініціалізовану змінну як індекс для масиву. Крім того, він може вийти з ладу навіть при налагодженні (наприклад, деякий налагоджувач / компілятор буде ініціалізуватися з певними шаблонами, щоб полегшити побачити, коли він виходить з ладу). На моєму прикладі, дивно, що використання bool невидиме: Оптимізатор вирішив використовувати його в обчисленні, відсутньому у вихідному коді.
Ремц

3
@Remz Я просто використовую масив, щоб показати, що згенерований код може бути еквівалентним, не припускаючи, що хтось насправді це напише.
Бармар

1
@Remz Сформулювати boolдо intз *(int *)&boolValueі роздрукувати його для налагодження, дивіться , якщо це що - то інше , ніж 0або 1коли він виходить з ладу. Якщо це так, це в значній мірі підтверджує теорію про те, що компілятор оптимізує вбудований інтерфейс, якщо як масив, який пояснює, чому він виходить з ладу.
Хавенард

2
@MSalters: std::bitset<8>не дає мені приємних імен для всіх моїх різних прапорів. Залежно від того, що вони є, це може бути важливим.
Мартін Боннер підтримує Моніку

15

Узагальнюючи своє запитання, ви запитуєте, чи дозволяє стандарт C ++ компілятору припускати, що boolможе мати лише внутрішнє числове подання '0' або '1' і використовує його таким чином?

Стандарт не говорить нічого про внутрішнє представництво а bool. Він визначає тільки те , що відбувається , коли Кастинг boolна int(або навпаки). Переважно, через ці цілісні перетворення (і те, що люди досить сильно покладаються на них) компілятор буде використовувати 0 і 1, але це не обов'язково (хоча він повинен дотримуватися обмежень будь-якого ABI нижчого рівня, який він використовує ).

Отже, компілятор, коли він бачить a, boolмає право вважати, що зазначений boolмістить будь-яку з бітових моделей ' true' або ' false' і робити все, що йому здається. Отже, якщо значення для trueі falseстановлять відповідно 1 і 0, компілятору дійсно дозволяється оптимізувати strlenдо 5 - <boolean value>. Можливі й інші веселі поведінки!

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

  • Ваш код працює так, як ви цього очікували
  • Ваш код не вдається випадково
  • Ваш код взагалі не виконується.

Дивіться, що повинен знати кожен програміст про невизначену поведінку

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