Допускает ли стандарт C ++ неинициализированный bool для сбоя программы?

430

Я знаю, что "неопределенное поведение" в 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? "true": "false" выглядело достаточно безопасно для меня, я предполагал: "Независимо от того, boolValue значение мусора находится в boolValue не имеет значения, так как оно все равно будет иметь значение true или false".

Я настроил пример Compiler Explorer, который показывает проблему в разборке, вот полный пример. Примечание: чтобы воспроизвести проблему, я обнаружил, что сработала комбинация с использованием 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;
}

Проблема возникает из-за оптимизатора: было достаточно умно сделать вывод, что строки "истина" и "ложь" отличаются только по длине на 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" и использовать его таким образом?

Или это случай, определяемый реализацией, и в этом случае реализация предполагала, что все ее значения bool будут когда-либо содержать только 0 или 1, а любое другое значение является неопределенной территорией поведения?

  • 180
    Это отличный вопрос. Это убедительная иллюстрация того, что неопределенное поведение не просто теоретическая проблема. Когда люди говорят, что в результате UB может произойти что-либо, это «что-нибудь» может быть действительно удивительным. Можно предположить, что неопределенное поведение все еще проявляется предсказуемым образом, но в наши дни с современными оптимизаторами это совсем не так. ОП потратил время на создание MCVE, тщательно исследовал проблему, осмотрел разборку и задал четкий, прямой вопрос об этом. Не могу просить больше.
  • 6
    Обратите внимание, что требование «ненулевое значение равно true » является правилом для логических операций, включая «присваивание bool» (которое может неявно вызывать static_cast<bool>() зависимости от специфики). Однако это не требование о внутреннем представлении bool выбранного компилятором.
Показать ещё 10 комментариев
Теги:
abi
undefined-behavior
llvm

6 ответов

257
Лучший ответ

Да, ISO C++ позволяет (но не требует) реализации сделать этот выбор.

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

Это интересный вопрос о том, как работают реальные реализации, но помните, что даже если бы ответ был другим, ваш код все равно был бы небезопасным, потому что современный C++ не является переносимой версией языка ассемблера.


Вы компилируете для x86-64 System V ABI, который указывает, что bool как функция arg в регистре представлена битовыми комбинациями false=0 и true=1 в младших 8 битах регистра 1. В памяти bool - это 1-байтовый тип, который также должен иметь целочисленное значение 0 или 1.

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

В ISO C++ это не указано, но это решение ABI широко распространено, потому что оно делает bool-> int преобразование дешевым (просто расширение zero-). Мне не известны никакие ABI, которые не позволяют компилятору принимать 0 или 1 для bool, для любой архитектуры (не только для x86). Он позволяет оптимизировать, например !mybool с xor eax,1 чтобы перевернуть !mybool бит: любой возможный код, который может перевернуть бит/целое число /bool между 0 и 1 в одной инструкции CPU. Или компилирование 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++ формально позволяет вам hexdump представлять объекты без Любое UB. Приведение указателей для копирования представления объекта отличается от назначения char foo = my_bool, конечно, так что логическое значение 0 или 1 не произойдет, и вы получите необработанное представление объекта.)

Вы частично "спрятали" UB на этом пути выполнения от компилятора с помощью noinline. Тем не менее, даже если он не встроен, межпроцедурная оптимизация может сделать версию функции зависимой от определения другой функции. (Во-первых, clang создает исполняемый файл, а не разделяемую библиотеку Unix, где может происходить взаимное расположение символов. Во-вторых, определение внутри определения class{} поэтому все единицы перевода должны иметь одно и то же определение. Как и в случае ключевого слова inline.)

Таким образом, компилятор может ud2 только ret или ud2 (недопустимая инструкция) в качестве определения для main, потому что путь выполнения, начинающийся с вершины main неизбежно встречает Undefined Behavior. (Что компилятор может видеть во время компиляции, если он решил следовать по пути через встроенный конструктор non-.)

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

GCC и Clang на практике иногда фактически ud2 на UB, вместо того, чтобы даже пытаться сгенерировать код для путей выполнения, которые не имеют смысла. Или для случаев, подобных падению конца void функции non-, gcc иногда пропускает команду ret. Если вы думали, что "моя функция просто вернется с мусором в RAX", вы сильно ошибаетесь. Современные компиляторы C++ больше не рассматривают язык как переносимый язык ассемблера. Ваша программа действительно должна быть действительной C++, не делая предположений о том, как автономная не встроенная версия вашей функции может выглядеть в asm.

Еще один забавный пример: почему невыравниваемый доступ к памяти mmap иногда вызывает ошибку в AMD64? , x86 не ошибается на целых числах без выравнивания, верно? Так почему проблема с неправильным uint16_t*? Потому что alignof(uint16_t) == 2, и нарушение этого предположения привело к segfault при автоматической векторизации с SSE2.

Смотрите также, что должен знать каждый программист на C о неопределенном поведении # 1/3, статья разработчика Clang.

Ключевой момент: если компилятор заметил UB во время компиляции, он мог бы "прервать" (испустить удивительный asm) путь через ваш код, который вызывает UB, даже если он нацелен на ABI, где любой битовый шаблон является допустимым представлением объекта для bool.

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

UB, видимый во время компиляции, всегда опасен, и очень трудно быть уверенным (с оптимизацией во время компоновки), что вы действительно скрыли UB от компилятора и, таким образом, можете решить, какой тип asm он сгенерирует.

Не быть чрезмерно драматичным; часто компиляторы позволяют вам сойтись с некоторыми вещами и генерировать код, который вы ожидаете, даже когда что-то не так. Но, возможно, это будет проблемой в будущем, если разработчики компиляторов реализуют некоторую оптимизацию, которая получает больше информации о диапазонах значений (например, переменная отрицательна non-, возможно, позволяя оптимизировать расширение знака для свободного расширения zero- на x86-64). Например, в текущих gcc и clang выполнение tmp = a+INT_MIN не позволяет им оптимизировать a<0 как всегда верное, только то, что tmp всегда отрицателен. (Таким образом, они не возвращаются от входных данных вычисления для получения информации о диапазоне, только на результатах, основанных на допущении отсутствия переполнения int range_check(int a, int *sink1, int *sink2) {%0A++++int tmp+%3D+(a++ INT_MIN)%3B++//a+>%3D+1 to avoid UB%0A++++//tmp+is definitely negative%0A++++//a+is definitely positive%0A++++if+(tmp+> 0) *sink1+%3D+0%3B++ //optimized away%0A++++if+(a+< 0) *sink2+%3D+0%3B++//not optimized away%0A++++return tmp; } //it seems that gcc and+Clang don!'t derive range info for a //based on `a-12345%60+not overflowing, //but they do derive range info for the result. int signed_overflow_rangecheck(int a, int *sink) {%0A++++int tmp+%3D+(a+- 12345);%0A++++//if+((unsigned)a+=%3D+0x80000000UL) *sink=0;%0A++++if+(tmp+> 0x7ffffff0) *sink+%3D+1%3B++//optimized out: it+Can!'t have wrapped to a+positive this+Close to INT_MAX%0A++++if+(a+< (INT_MIN+12)) *sink+%3D+2%3B++//not optimized out. But `a%60+this+Close to INT_MIN would mean a-12345 wrapped.%0A++++return tmp; } '),l:'5',n:'0',o:'C++ source #1',t:'0')),k:37.77562439622385,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang700,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),fontScale:1.2899450879999999,lang:c++,libs:!(),options:'-xc -Wall -Wextra+-O3 -std=gnu11 -march=znver1',source:1),l:'5',n:'0',o:'x86-64+Clang 7.0.0+(Editor #1,+Compiler+#1)+C++',t:'0')),k:30.92627232139171,l:'4',m:100,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g82,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),fontScale:1.2899450879999999,lang:c++,libs:!(),options:'-Wall -Wextra+-O3 -std=gnu++11 -fverbose-asm',source:1),l:'5',n:'0',o:'x86-64 gcc 8.2+(Editor #1,+Compiler+#2)+C++',t:'0')),k:31.29810328238445,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',m:100,n:'0',o:'',t:'0')),version:4 rel=noreferrer>со знаком: int range_check(int a, int *sink1, int *sink2) {%0A++++int tmp+%3D+(a++ INT_MIN)%3B++//a+>%3D+1 to avoid UB%0A++++//tmp+is definitely negative%0A++++//a+is definitely positive%0A++++if+(tmp+> 0) *sink1+%3D+0%3B++ //optimized away%0A++++if+(a+< 0) *sink2+%3D+0%3B++//not optimized away%0A++++return tmp; } //it seems that gcc and+Clang don!'t derive range info for a //based on `a-12345%60+not overflowing, //but they do derive range info for the result. int signed_overflow_rangecheck(int a, int *sink) {%0A++++int tmp+%3D+(a+- 12345);%0A++++//if+((unsigned)a+=%3D+0x80000000UL) *sink=0;%0A++++if+(tmp+> 0x7ffffff0) *sink+%3D+1%3B++//optimized out: it+Can!'t have wrapped to a+positive this+Close to INT_MAX%0A++++if+(a+< (INT_MIN+12)) *sink+%3D+2%3B++//not optimized out. But `a%60+this+Close to INT_MIN would mean a-12345 wrapped.%0A++++return tmp; } '),l:'5',n:'0',o:'C++ source #1',t:'0')),k:37.77562439622385,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang700,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),fontScale:1.2899450879999999,lang:c++,libs:!(),options:'-xc -Wall -Wextra+-O3 -std=gnu11 -march=znver1',source:1),l:'5',n:'0',o:'x86-64+Clang 7.0.0+(Editor #1,+Compiler+#1)+C++',t:'0')),k:30.92627232139171,l:'4',m:100,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g82,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),fontScale:1.2899450879999999,lang:c++,libs:!(),options:'-Wall -Wextra+-O3 -std=gnu++11 -fverbose-asm',source:1),l:'5',n:'0',o:'x86-64 gcc 8.2+(Editor #1,+Compiler+#2)+C++',t:'0')),k:31.29810328238445,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',m:100,n:'0',o:'',t:'0')),version:4 rel=noreferrer>пример на Godbolt. Я не знаю, является ли это преднамеренным удобством для пользователя или просто пропустил оптимизацию.)

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

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

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


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

(Другие ABI здесь делают другой выбор. Некоторые требуют, чтобы узкие целочисленные типы были zero- или расширены знаком для заполнения регистра при передаче или возвращении из функций, таких как MIPS64 и PowerPC64. См. Последний раздел этого x86-64. ответ, который сравнивает с теми более ранними МСА.)

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

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

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


Сноска 2: После того, как ветвление, вы бы просто иметь 4-байтовые mov -immediate, или 4-байтовый +--байтовый магазин. Длина указана в значениях ширины магазина + смещения.

OTOH, glibc memcpy выполнит две 4-байтовых загрузки/сохранения с перекрытием, которое зависит от длины, так что это действительно в конечном итоге делает все это свободным от условных ветвей в логическом значении. См. L(between_4_7): блок в glibc memcpy/memmove. Или, по крайней мере, используйте тот же способ для логического значения в ветвлении 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/

Чтобы найти использование неинициализированных данных, используйте Address 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, в которой использовались только cmov, indexing и два хранилища, это могло бы не cmov.

Valgrind memcheck также будет искать такую проблему, опять же не жалуясь, если программа просто копирует неинициализированные данные. Но он говорит, что обнаружит, когда "условный переход или перемещение зависит от неинициализированных значений", чтобы попытаться поймать любое внешне видимое поведение, которое зависит от неинициализированных данных.

Возможно, идея не отмечать только загрузку состоит в том, что структуры могут иметь заполнение, и копирование всей структуры (включая заполнение) с широкой векторной загрузкой/сохранением не является ошибкой, даже если отдельные элементы были записаны только по одному за раз. На уровне asm информация о том, что было дополнением и что на самом деле является частью значения, была потеряна.

  • 0
    gcc и clang на практике иногда испускают ud2. Или для случаев, таких как выпадение из конца не пустых функций ... ---> Я хочу соблазнить это конкретное предложение, на самом деле довольно часто получить SIGILL если вы игнорируете это предупреждение компилятора. Я уверен, что почти все сталкивались с этим раньше.
  • 0
    @liliscent: хорошее предположение, что такое поведение, когда UB виден во время компиляции, в корне отличается от поведения компиляторов и, вероятно, удивляет многих людей. (Мне это нравится; шумная ошибка - это хорошо, особенно когда нет ясного способа интерпретировать что-либо в источнике, например, выпадать из конца функции.)
Показать ещё 9 комментариев
52

Компилятору разрешается предполагать, что логическое значение, переданное в качестве аргумента, является допустимым логическим значением (то есть тем, которое было инициализировано или преобразовано в true или false). true значение не обязательно должно совпадать с целым числом 1 - действительно, могут быть различные представления true и false - но параметр должен быть некоторым допустимым представлением одного из этих двух значений, где "действительное представление" определяется реализацией.

Поэтому, если вам не удастся инициализировать bool, или если вам удастся перезаписать его с помощью какого-либо указателя другого типа, то предположения компилятора будут неверными, и последует Undefined Behavior. Вы были предупреждены:

50) Использование значения bool способами, описанными в этом международном стандарте как "неопределенные", например, путем проверки значения неинициализированного автоматического объекта, может привести к тому, что он будет вести себя так, как если бы он не был ни истинным, ни ложным. (Сноска к пункту 6 §6.9.1, Основные типы)

  • 11
    « true значение не обязательно должно совпадать с целым числом 1», что вводит в заблуждение. Конечно, фактическая битовая комбинация может быть чем-то другим, но при неявном преобразовании / повышении (единственный способ увидеть значение, отличное от true / false ), true всегда равно 1 , а false всегда 0 . Конечно, такой компилятор также не сможет использовать хитрость, которую пытался использовать этот компилятор (используя тот факт, что фактическая битовая комбинация bool может быть только 0 или 1 ), так что это как бы не имеет отношения к проблеме OP.
  • 3
    @ShadowRanger Вы всегда можете проверить представление объекта напрямую.
Показать ещё 9 комментариев
46

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

Ошибка в вызывающей функции, и она может быть обнаружена путем проверки кода или статического анализа вызывающей функции. Используя ссылку на ваш компилятор, компилятор gcc 8.2 обнаруживает ошибку. (Может быть, вы могли бы отправить отчет об ошибке в Clang, что он не находит проблему).

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

NB. Ответ на вопрос "Может ли неопределенное поведение вызвать _____?" всегда "да". Это буквально определение неопределенного поведения.

  • 2
    Верно ли первое предложение? Простое копирование неинициализированного bool вызывает UB?
  • 10
    @JoshuaGreen см. [Dcl.init] / 12 «Если в результате оценки получено неопределенное значение, поведение не определено, за исключением следующих случаев:» (и ни в одном из этих случаев нет исключения для bool ). Копирование требует оценки источника
Показать ещё 6 комментариев
21

В bool разрешено хранить только значения 0 или 1, и сгенерированный код может предполагать, что он будет содержать только одно из этих двух значений. Код, сгенерированный для троичной переменной в присваивании, может использовать значение в качестве индекса в массиве указателей на две строки, то есть его можно преобразовать во что-то вроде:

     // 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 невидимо: оптимизатор решил использовать его в вычислениях, которых нет в исходном коде.
Показать ещё 9 комментариев
15

Резюмируя ваш вопрос, вы спрашиваете: позволяет ли стандарт C++, чтобы компилятор предполагал, что bool может иметь только внутреннее числовое представление "0" или "1" и использовать его таким образом?

Стандарт ничего не говорит о внутреннем представлении bool. Он определяет только то, что происходит при приведении bool к int (или наоборот). В основном, из-за этих интегральных преобразований (и того факта, что люди довольно сильно на них полагаются), компилятор будет использовать 0 и 1, но это не обязательно (хотя он должен уважать ограничения любого ABI более низкого уровня, который он использует).

Таким образом, компилятор, когда он видит bool имеет право считать, что указанный bool содержит либо " true ", либо " false " битовый паттерн, и делать все, что ему нравится. Поэтому, если значения true и false равны 1 и 0 соответственно, компилятору действительно разрешено оптимизировать strlen до 5 - <boolean value>. Другие забавные поведения возможны!

Как неоднократно указывается здесь, неопределенное поведение имеет неопределенные результаты. В том числе, но не ограничивается

  • Ваш код работает так, как вы ожидали
  • Ваш код не работает в случайное время
  • Ваш код вообще не запускается.

Посмотрите, что каждый программист должен знать о неопределенном поведении

0

Вы должны найти способ проверить, не введен ли ввод в вектор.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню