Я знаю, что "неопределенное поведение" в 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», и использовать его таким образом?
Или это случай, определяемый реализацией, и в этом случае реализация предполагала, что все ее bools всегда будут содержать только 0 или 1, а любое другое значение является неопределенной территорией поведения?
Обратите внимание на то, что требование «ненулевое значение соответствует true» является правилом для логических операций, включая «присвоение логическому типу» (которое может неявно вызывать static_cast<bool>() в зависимости от специфики). Однако это не требование к внутреннему представлению bool, выбранному компилятором.
Комментарии не подлежат расширенному обсуждению; этот разговор был переехал в чат.
По очень похожему примечанию, это "забавный" источник двоичной несовместимости. Если у вас есть ABI A, который дополняет значения нулями перед вызовом функции, но компилирует функции таким образом, что предполагает, что параметры дополняются нулями, и ABI B, который противоположен (не заполняет нулями, но не предполагает нулевое значение) -padded parameters), он будет работать главным образом, но функция, использующая B ABI, вызовет проблемы, если она вызовет функцию с использованием ABI, которая принимает «маленький» параметр. IIRC, у вас это на x86 с clang и ICC.
@TLW: Хотя Стандарт не требует, чтобы реализации предоставляли какие-либо средства вызова или вызова из внешнего кода, было бы полезно иметь средства определения таких вещей для реализаций, где они актуальны (реализации, в которых такие детали не соответствующие могут игнорировать такие атрибуты).
Нет смысла говорить о стандарте, допускающем что-то в реализации, кроме создания видимых извне эффектов в соответствии с правилом «как если бы». Стандарт - это спецификация преобразования текстов в наборы последовательностей эффектов. (Считается, что тексты в области отображения имеют определенное поведение.)
@philipxy: Вопрос в том, что представляет собой «эффект». Я бы посоветовал «вызвать внешнюю функцию с именем foo, которая принимает bool и int, но ожидает, что маленькие аргументы будут расширены вызывающей стороной до int», и это должен быть «эффект», поведение которого будет заключаться в том, чтобы делать то, с чем происходит эта именованная функция. Стандарту не нужно заботиться о деталях того, на что влияют такие функции, помимо того факта, что реализация может выполнять свои обязательства, фактически выполняя указанный вызов функции.
@supercat Я не понимаю твою точку зрения. Под «эффектом» я имел в виду те, которые составляют «наблюдаемое поведение», технический термин, используемый при описании семантика через правило "как если бы". Это не имеет ничего общего с реализацией, за исключением того, что реализация должна влиять на эффекты. призыв не является эффектом. Вызов представляет собой некоторый синтаксис, которому соответствует этап выполнения абстрактной машины, но этот этап не является наблюдаемым поведением и не должен соответствовать чему-либо в выполнении реализации.
@philipxy: Я хочу сказать, что вызов функции с определенным именем с использованием определенного соглашения о вызовах, когда в исходном тексте не существует функции с таким именем, следует рассматривать как «наблюдаемое поведение» в реализациях, поддерживающих внешние функция вызывает и может найти функцию с таким именем. Большинство программ на C (включая почти все программы для автономных реализаций) полагаются на способность «абстрактной машины» взаимодействовать с вещами, не подпадающими под юрисдикцию Стандарта. В Стандарте нет необходимости указывать, как работают внешние вещи ...
@supercat "Должен" не "есть". По поводу «вашей точки зрения»: вы по-прежнему не связываете свои комментарии с моим исходным комментарием, который правильно сообщает спрашивающему, что у него неправильное представление о том, что говорится в стандарте, и говорится о том, что на самом деле имеет отношение к семантике программы. Конечно, можно спросить о других понятиях «внутреннего» (как отвечает ПетерКордес), но в текущем вопросе не хватает понимания более фундаментальных понятий - сначала нужно понять абстрактную машину и правило «как если бы».
Это вполне возможно, если bool находится в стеке, но указатель стека указывает на недоступную страницу. Хотя это вещь архитектуры процессора, а не вещь C++.
Да, потому что на некоторых платформах есть битовый флаг для адреса памяти, который обозначает его как неинициализированный, и чтение неинициализированной памяти приведет к ABEND (аварийному завершению) вашей программы. UB (неопределенное поведение) опасно. Это может привести к сбою вашей программы или, что еще хуже ... может показаться, что она работает правильно. Это могло даже разрушить Землю. И это плохо, потому что там я храню все свои вещи.
Каждый раз, когда я слышу, как кто-то говорит «он будет инициализирован как мусор», я заметно вздрагиваю. Вот почему. :)
Это в стеке. Ваш код действительно может дать сбой здесь, на процессоре Itanium.
Мне вспоминается одна из самых странных ошибок C++, которые у меня были за последнее время. Код if (a && b) { ... } вёл себя странно, как будто b был ложным, когда я думал, что это должно быть правдой, и в отчаянии я добавил распечатку отладки (использование отладчика было неудобно), чтобы сделать его if (b) printf("b is true\n"); if (a && b) { ... }, который напечатал b is true, хотя следующий тест все еще действовал так, как если бы b был ложным. Оказалось, что b был равен 2, и gcc выдавал команду test-low-bit в одном месте и команду! = 0 в другом.





Компилятору разрешено предполагать, что логическое значение, переданное в качестве аргумента, является допустимым логическим значением (то есть тем, которое было инициализировано или преобразовано в true или false). Значение true не обязательно должно совпадать с целым числом 1 - действительно, могут быть различные представления true и false - но параметр должен быть некоторым допустимым представлением одного из этих двух значений, где «допустимое представление» определяется реализацией.
Поэтому, если вам не удалось инициализировать bool или если вам удастся перезаписать его с помощью некоторого указателя другого типа, тогда предположения компилятора будут неправильными, и последует неопределенное поведение. Вас предупредили:
50) Using a bool value in ways described by this International Standard as “undefined”, such as by examining the value of an uninitialized automatic object, might cause it to behave as if it is neither true nor false. (Footnote to para 6 of §6.9.1, Fundamental Types)
«Значение true не обязательно должно совпадать с целым числом 1» вводит в заблуждение. Конечно, фактический битовый шаблон мог будет чем-то другим, но при неявном преобразовании / продвижении (единственный способ увидеть значение, отличное от true / false), true - всегда 1, а false - всегда 0.. Конечно, такой компилятор также не смог бы использовать трюк, который этот компилятор пытался использовать (учитывая тот факт, что фактический битовый шаблон bool мог быть только 0 или 1), поэтому это не имеет отношения к проблеме OP.
@ShadowRanger Вы всегда можете напрямую проверить представление объекта.
@shadowranger: я хочу сказать, что за реализацию отвечает реализация. Если он ограничивает допустимые представления true битовой комбинацией 1, это его прерогатива. Если он выберет какой-то другой набор представлений, тогда он действительно не сможет использовать отмеченную здесь оптимизацию. Если он выберет именно это представление, то сможет. Это только должно быть внутренне непротиворечивым. Вы может исследуете представление bool, копируя его в массив байтов; это не UB (но определяется реализацией)
Да, оптимизирующие компиляторы (то есть реальная реализация C++) часто иногда генерируют код, который зависит от bool, имеющего битовый шаблон 0 или 1. Они не повторно логически обрабатывают bool каждый раз, когда читают его из памяти (или регистра, содержащего функцию arg). Вот что говорит этот ответ. Примеры: gcc4.7 + может оптимизировать return a||b до or eax, edi в функции, возвращающей bool, или MSVC может оптимизировать a&b до test cl, dl. test x86 - это побитовыйand, поэтому, если тест cl=1 и dl=2 устанавливает флаги в соответствии с cl&dl = 0.
Суть неопределенное поведение заключается в том, что компилятору разрешено делать гораздо больше выводов по этому поводу, например предположить, что кодовый путь, который привел бы к доступу к неинициализированному значению, вообще никогда не принимается, как обеспечение того, чтобы это как раз и является обязанностью программиста. Так что дело не только в возможности того, что значения нижнего уровня могут отличаться от нуля или единицы.
(Я не знаком с C++). Есть ли способ (во время выполнения) подтвердить, что значение не инициализировано? Или это возможно только при статическом анализе языка?
@burnsba: ни C, ни C++ не предоставляют никакого механизма времени выполнения для проверки неинициализированных значений. При отсутствии аппаратной поддержки (что, по меньшей мере, редкость) любой такой механизм потребовал бы значительных затрат. Статический анализ также не всегда может выявить ошибку, но визуальный осмотр покажет вам переменные, не инициализированные в их объявлениях. Если вы всегда предоставляете инициализатор, вы не страдаете от этой конкретной проблемы.
@Holger: "Суть" неопределенного поведения заключается в том, что во избежание необходимости от компиляторов тратить на это больше усилий, чем было бы необходимо для наилучшего обслуживания пользователей, Стандарт воздерживается от каких-либо требований. Авторы качественных компиляторов, по-видимому, должны быть в лучшем положении, чем авторы Стандарта, в состоянии распознать, когда их клиенты выиграют от более строгих поведенческих гарантий, чем предписано Стандартом, и когда «устранение мертвых ветвей» на основе UB будет более полезным.
@BurnsBA: некоторые реализации (включая gcc и clang) могут добавлять инструментарий времени выполнения для обнаружения некоторых форм UB, которые не всегда обнаруживаются во время компиляции. например gcc -fsanitize=undefined -O3 foo.c. См. developers.redhat.com/blog/2014/10/16/…. Чтобы найти использование неинициализированных данных, в clang / LLVM есть Address Sanitizer и Memory Sanitizer. github.com/google/sanitizers/wiki/MemorySanitizer показывает примеры обнаружения неинициализированного чтения памяти.
@supercat Я имел в виду: слишком многие программисты могут подумать, что худшее, что может случиться, - это то, что неинициализированный логический объект может иметь другое значение, чем два допустимых. Но эффекты УБ могут быть произвольными. Например. когда у вас есть if (condition1) foo=expression; /* the only initialization of foo */ if (condition2) bar(foo); /* the only use of foo */, компилятор может предположить, что condition2 подразумевает condition1, без необходимости доказывать это. При отсутствии других побочных эффектов он мог бы преобразовать его в if (condition2) bar(expression);; он может даже использовать это предположение в последующем коде.
@Holger: при использовании компиляторов, которые не предназначены для написания программ, которые когда-либо будут обрабатывать ввод из ненадежных источников, это, безусловно, правда. Программистам, безусловно, нужно знать, что многие компиляторы «общего назначения» подходят только для нескольких специализированных целей, если их оптимизатор не отключен.
Bool может содержать только зависящие от реализации значения, используемые внутри для 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.
@SidS Спасибо. Теоретически внутренние представления могут быть противоположны тому, как они приводят к целым числам или от них, но это было бы неверно.
Вы правы, и ваш пример тоже рухнет. Однако для обзора кода «видно», что вы используете неинициализированную переменную в качестве индекса для массива. Кроме того, он выйдет из строя даже при отладке (например, какой-то отладчик / компилятор будет инициализироваться с определенными шаблонами, чтобы было легче увидеть, когда он выйдет из строя). В моем примере удивительно то, что использование bool невидимо: оптимизатор решил использовать его в вычислениях, отсутствующих в исходном коде.
@Remz Я просто использую массив, чтобы показать, чему сгенерированный код может быть эквивалентен, не предполагая, что кто-то действительно напишет это.
@Remz Переделайте bool в int с помощью *(int *)&boolValue и распечатайте его для целей отладки, посмотрите, если это что-то другое, кроме 0 или 1, когда он выйдет из строя. Если это так, это в значительной степени подтверждает теорию о том, что компилятор оптимизирует встроенный if как массив, который объясняет, почему он дает сбой.
@Havenard, int, вероятно, будет больше, чем bool, так что это ничего не доказывает.
@SidS: Это так? int часто является логическим размером машинного слова, а short является оптимизированным для хранения вариантом. bool в любом случае не предназначен для оптимизации хранения; даже 1-байтовый bool тратит минимум 87,5%. Поскольку он не предназначен для оптимизации хранилища, имеет смысл иметь bool также с естественным размером объекта, то есть sizeof(int)==sizeof(bool).
@MSalters sizeof(bool) == 1 на большинстве платформ. Было бы ужасно, если бы 8 булевых значений, которые я вставил в какую-то структуру, потратили бы 31 байт вместо 7.
@TavianBarnes Конечно, если вы помещаете много булевых значений в структуру, лучше всего использовать битовые поля или явное битовое маскирование, чтобы минимизировать потери.
@TavianBarnes: Для этого есть std::bitset<8>, оптимизированный по пространству. Как и std::vector<bool>.
@MSalters Верно, забыл, что это вопрос C++, - думал С.
@MSalters: std::bitset<8> не дает мне хороших имен для всех моих разных флагов. В зависимости от того, что они из себя представляют, это может быть важно.
false, а последний конвертируется в true, все готово.
@LightnessRacesinOrbit Я добавил пояснение, спасибо.
@Barmar Хороший
Сама функция верна, но в вашей тестовой программе оператор, вызывающий функцию, вызывает неопределенное поведение, используя значение неинициализированной переменной.
Ошибка заключается в вызывающей функции, и ее можно обнаружить с помощью анализа кода или статического анализа вызывающей функции. Используя ссылку на обозреватель компилятора, компилятор gcc 8.2 обнаруживает ошибку. (Возможно, вы могли бы отправить отчет об ошибке против clang, что проблема не обнаружена).
Неопределенное поведение означает, что может произойти что-либо, что включает сбой программы через несколько строк после события, вызвавшего неопределенное поведение.
NB. Ответ на вопрос «Может ли неопределенное поведение вызывать _____?» всегда «Да». Это буквально определение неопределенного поведения.
Верно ли первое предложение? Просто ли копирование неинициализированного bool запускает UB?
@JoshuaGreen см. [Dcl.init] / 12 «Если неопределенное значение создается оценкой, поведение не определено, за исключением следующих случаев:» (и ни один из этих случаев не имеет исключения для bool). Копирование требует оценки источника
@JoshuaGreen Причина в том, что у вас может быть платформа, которая вызывает аппаратный сбой, если вы обращаетесь к некоторым недопустимым значениям для некоторых типов. Их иногда называют «представлениями ловушек».
Itanium, хотя и неясный, - это ЦП, который все еще находится в производстве, имеет значения прерывания и имеет как минимум два полусовременных компилятора C++ (Intel / HP). Он буквально имеет значения true, false и not-a-thing для логических значений.
С другой стороны, ответ на вопрос «Требует ли стандарт, чтобы все компиляторы обрабатывали что-то определенным образом?» Обычно «нет», даже / особенно в тех случаях, когда очевидно, что любой качественный компилятор должен это делать; чем очевиднее что-то, тем меньше у авторов Стандарта должно быть необходимости говорить об этом.
Проголосовали за последний абзац. Говорит все, правда.
@ M.M, спасибо за ссылку, похоже, вы правы, абсолютно. Я нашел эта страница более подробно. Поскольку он специально называет unsigned narrow char "особенным", я считаю это убедительным доказательством того, что bool, ну, нет особенный.
@JoshuaGreen: Авторы Стандарта не думали, что людям, пишущим реализации, в которых имеет смысл рассматривать некоторые другие типы как особые, потребуется разрешение Стандарта делать то, что имеет смысл. Авторы Стандарта не намеревались подразумевать, что все, что реализация может делать в соответствии со Стандартом, следует рассматривать как «разумное».
Обобщая ваш вопрос, вы спрашиваете: позволяет ли стандарт C++ компилятору предполагать, что bool может иметь только внутреннее числовое представление «0» или «1» и использовать его таким образом?
Стандарт ничего не говорит о внутреннем представлении bool. Он только определяет, что происходит при преобразовании bool в int (или наоборот). В основном из-за этих интегральных преобразований (и того факта, что люди довольно сильно полагаются на них) компилятор будет использовать 0 и 1, но это не обязательно (хотя он должен соблюдать ограничения любого ABI нижнего уровня, который он использует ).
Итак, компилятор, когда он видит bool, имеет право считать, что указанный bool содержит либо битовые шаблоны «true», либо «false», и делать все, что ему хочется. Таким образом, если значения для true и false равны 1 и 0 соответственно, компилятору действительно разрешено оптимизировать strlen до 5 - <boolean value>. Возможны другие забавные формы поведения!
Как здесь неоднократно упоминалось, неопределенное поведение приводит к неопределенным результатам. В том числе, но не ограничиваясь
См. Что каждый программист должен знать о неопределенном поведении
Отличный вопрос. Это убедительная иллюстрация того, что неопределенное поведение - это не просто теоретическая проблема. Когда люди говорят, что в результате UB может случиться что угодно, это «все» может быть довольно неожиданным. Можно предположить, что неопределенное поведение по-прежнему проявляется предсказуемым образом, но в наши дни с современными оптимизаторами это совсем не так. OP потратил время на создание MCVE, тщательно исследовал проблему, проверил разборку и задал четкий и прямой вопрос по этому поводу. Не могу просить большего.