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

Я знаю, что "неопределенное поведение" в 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, а любое другое значение является неопределенной территорией поведения?

Отличный вопрос. Это убедительная иллюстрация того, что неопределенное поведение - это не просто теоретическая проблема. Когда люди говорят, что в результате UB может случиться что угодно, это «все» может быть довольно неожиданным. Можно предположить, что неопределенное поведение по-прежнему проявляется предсказуемым образом, но в наши дни с современными оптимизаторами это совсем не так. OP потратил время на создание MCVE, тщательно исследовал проблему, проверил разборку и задал четкий и прямой вопрос по этому поводу. Не могу просить большего.

John Kugelman 10.01.2019 03:04

Обратите внимание на то, что требование «ненулевое значение соответствует true» является правилом для логических операций, включая «присвоение логическому типу» (которое может неявно вызывать static_cast<bool>() в зависимости от специфики). Однако это не требование к внутреннему представлению bool, выбранному компилятором.

Euro Micelli 10.01.2019 04:48

Комментарии не подлежат расширенному обсуждению; этот разговор был переехал в чат.

Samuel Liew 11.01.2019 13:28

По очень похожему примечанию, это "забавный" источник двоичной несовместимости. Если у вас есть ABI A, который дополняет значения нулями перед вызовом функции, но компилирует функции таким образом, что предполагает, что параметры дополняются нулями, и ABI B, который противоположен (не заполняет нулями, но не предполагает нулевое значение) -padded parameters), он будет работать главным образом, но функция, использующая B ABI, вызовет проблемы, если она вызовет функцию с использованием ABI, которая принимает «маленький» параметр. IIRC, у вас это на x86 с clang и ICC.

TLW 12.01.2019 20:36

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

supercat 12.01.2019 23:14

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

philipxy 13.01.2019 03:22

@philipxy: Вопрос в том, что представляет собой «эффект». Я бы посоветовал «вызвать внешнюю функцию с именем foo, которая принимает bool и int, но ожидает, что маленькие аргументы будут расширены вызывающей стороной до int», и это должен быть «эффект», поведение которого будет заключаться в том, чтобы делать то, с чем происходит эта именованная функция. Стандарту не нужно заботиться о деталях того, на что влияют такие функции, помимо того факта, что реализация может выполнять свои обязательства, фактически выполняя указанный вызов функции.

supercat 14.01.2019 23:44

@supercat Я не понимаю твою точку зрения. Под «эффектом» я имел в виду те, которые составляют «наблюдаемое поведение», технический термин, используемый при описании семантика через правило "как если бы". Это не имеет ничего общего с реализацией, за исключением того, что реализация должна влиять на эффекты. призыв не является эффектом. Вызов представляет собой некоторый синтаксис, которому соответствует этап выполнения абстрактной машины, но этот этап не является наблюдаемым поведением и не должен соответствовать чему-либо в выполнении реализации.

philipxy 14.01.2019 23:56

@philipxy: Я хочу сказать, что вызов функции с определенным именем с использованием определенного соглашения о вызовах, когда в исходном тексте не существует функции с таким именем, следует рассматривать как «наблюдаемое поведение» в реализациях, поддерживающих внешние функция вызывает и может найти функцию с таким именем. Большинство программ на C (включая почти все программы для автономных реализаций) полагаются на способность «абстрактной машины» взаимодействовать с вещами, не подпадающими под юрисдикцию Стандарта. В Стандарте нет необходимости указывать, как работают внешние вещи ...

supercat 15.01.2019 00:15

@supercat "Должен" не "есть". По поводу «вашей точки зрения»: вы по-прежнему не связываете свои комментарии с моим исходным комментарием, который правильно сообщает спрашивающему, что у него неправильное представление о том, что говорится в стандарте, и говорится о том, что на самом деле имеет отношение к семантике программы. Конечно, можно спросить о других понятиях «внутреннего» (как отвечает ПетерКордес), но в текущем вопросе не хватает понимания более фундаментальных понятий - сначала нужно понять абстрактную машину и правило «как если бы».

philipxy 15.01.2019 00:32

Это вполне возможно, если bool находится в стеке, но указатель стека указывает на недоступную страницу. Хотя это вещь архитектуры процессора, а не вещь C++.

peterh 20.01.2019 09:16

Да, потому что на некоторых платформах есть битовый флаг для адреса памяти, который обозначает его как неинициализированный, и чтение неинициализированной памяти приведет к ABEND (аварийному завершению) вашей программы. UB (неопределенное поведение) опасно. Это может привести к сбою вашей программы или, что еще хуже ... может показаться, что она работает правильно. Это могло даже разрушить Землю. И это плохо, потому что там я храню все свои вещи.

Eljay 04.02.2019 23:25

Каждый раз, когда я слышу, как кто-то говорит «он будет инициализирован как мусор», я заметно вздрагиваю. Вот почему. :)

Lightness Races in Orbit 25.05.2019 15:58

Это в стеке. Ваш код действительно может дать сбой здесь, на процессоре Itanium.

Joshua 23.09.2019 20:09

Мне вспоминается одна из самых странных ошибок 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 в другом.

Steve Summit 29.10.2019 14:52
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
541
15
36 717
4

Ответы 4

Компилятору разрешено предполагать, что логическое значение, переданное в качестве аргумента, является допустимым логическим значением (то есть тем, которое было инициализировано или преобразовано в 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 10.01.2019 03:08

@ShadowRanger Вы всегда можете напрямую проверить представление объекта.

T.C. 10.01.2019 03:12

@shadowranger: я хочу сказать, что за реализацию отвечает реализация. Если он ограничивает допустимые представления true битовой комбинацией 1, это его прерогатива. Если он выберет какой-то другой набор представлений, тогда он действительно не сможет использовать отмеченную здесь оптимизацию. Если он выберет именно это представление, то сможет. Это только должно быть внутренне непротиворечивым. Вы может исследуете представление bool, копируя его в массив байтов; это не UB (но определяется реализацией)

rici 10.01.2019 03:28

Да, оптимизирующие компиляторы (то есть реальная реализация 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.

Peter Cordes 10.01.2019 09:21

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

Holger 10.01.2019 11:47

(Я не знаком с C++). Есть ли способ (во время выполнения) подтвердить, что значение не инициализировано? Или это возможно только при статическом анализе языка?

BurnsBA 10.01.2019 15:28

@burnsba: ни C, ни C++ не предоставляют никакого механизма времени выполнения для проверки неинициализированных значений. При отсутствии аппаратной поддержки (что, по меньшей мере, редкость) любой такой механизм потребовал бы значительных затрат. Статический анализ также не всегда может выявить ошибку, но визуальный осмотр покажет вам переменные, не инициализированные в их объявлениях. Если вы всегда предоставляете инициализатор, вы не страдаете от этой конкретной проблемы.

rici 10.01.2019 15:51

@Holger: "Суть" неопределенного поведения заключается в том, что во избежание необходимости от компиляторов тратить на это больше усилий, чем было бы необходимо для наилучшего обслуживания пользователей, Стандарт воздерживается от каких-либо требований. Авторы качественных компиляторов, по-видимому, должны быть в лучшем положении, чем авторы Стандарта, в состоянии распознать, когда их клиенты выиграют от более строгих поведенческих гарантий, чем предписано Стандартом, и когда «устранение мертвых ветвей» на основе UB будет более полезным.

supercat 11.01.2019 04:40

@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 показывает примеры обнаружения неинициализированного чтения памяти.

Peter Cordes 11.01.2019 05:27

@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 11.01.2019 08:46

@Holger: при использовании компиляторов, которые не предназначены для написания программ, которые когда-либо будут обрабатывать ввод из ненадежных источников, это, безусловно, правда. Программистам, безусловно, нужно знать, что многие компиляторы «общего назначения» подходят только для нескольких специализированных целей, если их оптимизатор не отключен.

supercat 11.01.2019 16:56

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 Спасибо. Теоретически внутренние представления могут быть противоположны тому, как они приводят к целым числам или от них, но это было бы неверно.

Barmar 10.01.2019 03:09

Вы правы, и ваш пример тоже рухнет. Однако для обзора кода «видно», что вы используете неинициализированную переменную в качестве индекса для массива. Кроме того, он выйдет из строя даже при отладке (например, какой-то отладчик / компилятор будет инициализироваться с определенными шаблонами, чтобы было легче увидеть, когда он выйдет из строя). В моем примере удивительно то, что использование bool невидимо: оптимизатор решил использовать его в вычислениях, отсутствующих в исходном коде.

Remz 10.01.2019 03:25

@Remz Я просто использую массив, чтобы показать, чему сгенерированный код может быть эквивалентен, не предполагая, что кто-то действительно напишет это.

Barmar 10.01.2019 03:28

@Remz Переделайте bool в int с помощью *(int *)&boolValue и распечатайте его для целей отладки, посмотрите, если это что-то другое, кроме 0 или 1, когда он выйдет из строя. Если это так, это в значительной степени подтверждает теорию о том, что компилятор оптимизирует встроенный if как массив, который объясняет, почему он дает сбой.

Havenard 10.01.2019 03:57

@Havenard, int, вероятно, будет больше, чем bool, так что это ничего не доказывает.

Sid S 10.01.2019 05:11

@SidS: Это так? int часто является логическим размером машинного слова, а short является оптимизированным для хранения вариантом. bool в любом случае не предназначен для оптимизации хранения; даже 1-байтовый bool тратит минимум 87,5%. Поскольку он не предназначен для оптимизации хранилища, имеет смысл иметь bool также с естественным размером объекта, то есть sizeof(int)==sizeof(bool).

MSalters 10.01.2019 21:06

@MSalters sizeof(bool) == 1 на большинстве платформ. Было бы ужасно, если бы 8 булевых значений, которые я вставил в какую-то структуру, потратили бы 31 байт вместо 7.

Tavian Barnes 10.01.2019 21:25

@TavianBarnes Конечно, если вы помещаете много булевых значений в структуру, лучше всего использовать битовые поля или явное битовое маскирование, чтобы минимизировать потери.

Barmar 10.01.2019 21:59

@TavianBarnes: Для этого есть std::bitset<8>, оптимизированный по пространству. Как и std::vector<bool>.

MSalters 10.01.2019 23:24

@MSalters Верно, забыл, что это вопрос C++, - думал С.

Barmar 11.01.2019 00:01

@MSalters: std::bitset<8> не дает мне хороших имен для всех моих разных флагов. В зависимости от того, что они из себя представляют, это может быть важно.

Martin Bonner supports Monica 11.01.2019 16:13
"Bool может содержать только значения 0 или 1" Почти. Логический тип может содержать только значения <значения, выбранные им для ложного> или <значения, выбранные им для истины> ". Пока impl является внутренне непротиворечивым, а первое преобразуется в false, а последний конвертируется в true, все готово.
Lightness Races in Orbit 07.10.2019 16:32

@LightnessRacesinOrbit Я добавил пояснение, спасибо.

Barmar 07.10.2019 16:52

@Barmar Хороший

Lightness Races in Orbit 07.10.2019 17:21

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

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

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

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

Верно ли первое предложение? Просто ли копирование неинициализированного bool запускает UB?

Joshua Green 10.01.2019 04:25

@JoshuaGreen см. [Dcl.init] / 12 «Если неопределенное значение создается оценкой, поведение не определено, за исключением следующих случаев:» (и ни один из этих случаев не имеет исключения для bool). Копирование требует оценки источника

M.M 10.01.2019 04:34

@JoshuaGreen Причина в том, что у вас может быть платформа, которая вызывает аппаратный сбой, если вы обращаетесь к некоторым недопустимым значениям для некоторых типов. Их иногда называют «представлениями ловушек».

David Schwartz 10.01.2019 12:15

Itanium, хотя и неясный, - это ЦП, который все еще находится в производстве, имеет значения прерывания и имеет как минимум два полусовременных компилятора C++ (Intel / HP). Он буквально имеет значения true, false и not-a-thing для логических значений.

MSalters 10.01.2019 21:03

С другой стороны, ответ на вопрос «Требует ли стандарт, чтобы все компиляторы обрабатывали что-то определенным образом?» Обычно «нет», даже / особенно в тех случаях, когда очевидно, что любой качественный компилятор должен это делать; чем очевиднее что-то, тем меньше у авторов Стандарта должно быть необходимости говорить об этом.

supercat 10.01.2019 22:23

Проголосовали за последний абзац. Говорит все, правда.

rici 12.01.2019 04:09

@ M.M, спасибо за ссылку, похоже, вы правы, абсолютно. Я нашел эта страница более подробно. Поскольку он специально называет unsigned narrow char "особенным", я считаю это убедительным доказательством того, что bool, ну, нет особенный.

Joshua Green 12.01.2019 14:11

@JoshuaGreen: Авторы Стандарта не думали, что людям, пишущим реализации, в которых имеет смысл рассматривать некоторые другие типы как особые, потребуется разрешение Стандарта делать то, что имеет смысл. Авторы Стандарта не намеревались подразумевать, что все, что реализация может делать в соответствии со Стандартом, следует рассматривать как «разумное».

supercat 15.01.2019 19:13

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

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

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

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

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

См. Что каждый программист должен знать о неопределенном поведении

Другие вопросы по теме