В C ++ Core Guidelines есть правило ES.20: всегда инициализировать объект .
Избегайте ошибок «до установки» и связанных с ними неопределенных действий. Избегайте проблем с пониманием сложной инициализации. Упростить рефакторинг.
Но это правило не помогает находить ошибки, оно только скрывает их.
Предположим, что у программы есть путь выполнения, в котором она использует неинициализированную переменную. Это ошибка. Помимо неопределенного поведения, это также означает, что что-то пошло не так, и программа, вероятно, не соответствует требованиям к продукту. Когда он будет развернут в производство, возможна потеря денег или, что еще хуже.
Как мы показываем ошибки? Мы пишем тесты. Но тесты не покрывают 100% путей выполнения, и тесты никогда не покрывают 100% входных данных программы. Более того, даже тест покрывает ошибочный путь выполнения - он все еще может пройти. В конце концов, это неопределенное поведение, неинициализированная переменная может иметь несколько допустимое значение.
Но в дополнение к нашим тестам у нас есть компиляторы, которые могут записывать что-то вроде 0xCDCDCDCD в неинициализированные переменные. Это немного улучшает уровень обнаружения тестов.
Еще лучше - есть такие инструменты, как Address Sanitizer, которые будут перехватывать все чтения неинициализированных байтов памяти.
И наконец, есть статические анализаторы, которые могут посмотреть на программу и сказать, что в этом пути выполнения есть чтение перед установкой.
Итак, у нас есть много мощных инструментов, но если мы инициализируем переменную - дезинфицирующие средства ничего не найдут .
int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.
// Another bug: use empty buffer after read error.
use(buffer);
Есть еще одно правило - если при выполнении программы возникает ошибка, программа должна умереть как можно скорее. Не нужно его поддерживать, просто рухнуть, написать аварийный дамп и передать его инженерам для расследования.
Инициализация переменных без необходимости делает противоположное - программа поддерживается, когда в противном случае она уже получит ошибку сегментации.
bytes_read
она не изменяется (поэтому она сохраняется равной нулю), почему это должно быть ошибкой? Программа все еще может продолжаться в том же духе до тех пор, пока она не ожидаетbytes_read!=0
этого неявно . Так что это нормально, дезинфицирующие средства не жалуются. С другой стороны, если программаbytes_read
не инициализирована заранее, программа не сможет продолжать в том же духе, поэтому отсутствие инициализацииbytes_read
фактически приводит к ошибке, которой раньше не было.\0
него, он глючит. Если это задокументировано, чтобы не иметь дело с этим, ваш код вызова глючит. Если вы исправите свой код вызова для проверкиbytes_read==0
перед вызовом use, то вы вернетесь к тому, с чего начали: ваш код глючит, если вы не инициализируетеbytes_read
, и безопасен, если вы это делаете. ( Обычно функции должны заполнять свои out-параметры даже в случае ошибки : не совсем. Часто выходы либо остаются одни, либо не определены.)err_t
возвращаемоеmy_read()
? Если в примере есть ошибка, вот и все.Ответы:
Ваши рассуждения ошибочны по нескольким причинам:
bytes_read
имеет значение,10
как и значение0xcdcdcdcd
.Идея, лежащая в основе указания всегда инициализировать переменные, состоит в том, чтобы включить эти две ситуации
Переменная содержит полезное значение с самого начала своего существования. Если вы объедините это с указанием объявлять переменную только тогда, когда вам это нужно, вы можете избежать того, чтобы будущие программисты по техническому обслуживанию попали в ловушку того, чтобы начать использовать переменную между ее объявлением и первым присваиванием, где переменная будет существовать, но не будет инициализирована.
Переменная содержит определенное значение, которое вы можете проверить позже, чтобы узнать,
my_read
обновила ли функция как значение. Без инициализации вы не сможете определить,bytes_read
действительно ли имеет допустимое значение, потому что вы не можете знать, с какого значения оно началось.источник
= 0;
. Цель совета - объявить переменную в том месте, где у вас будет для нее полезное значение, и немедленно назначить это значение. Это четко указано в следующих непосредственно правилах ES21 и ES22. Все эти три следует понимать как совместную работу; не как отдельные несвязанные правила.Вы написали, что «это правило не помогает находить ошибки, оно только скрывает их» - ну, цель правила не в том, чтобы находить ошибки, а в том, чтобы их избегать . И когда ошибка устраняется, ничего не скрывается.
Обсудим проблему с точки зрения вашего примера: предположим, что
my_read
функция имеет письменный контракт на инициализациюbytes_read
при любых обстоятельствах, но это не так в случае ошибки, поэтому она, по крайней мере, в этом случае неисправна. Вы намерены использовать среду времени выполнения, чтобы показать эту ошибку, не инициализируяbytes_read
параметр в первую очередь. Если вы точно знаете, что существует дезинфицирующее средство для адресов, это действительно возможный способ обнаружить такую ошибку. Чтобы исправить ошибку, нужно изменитьmy_read
функцию внутри.Но есть и другая точка зрения, которая, по крайней мере, в равной степени верна: ошибочное поведение возникает только из комбинации не инициализации
bytes_read
заранее и вызоваmy_read
после (с ожиданиемbytes_read
инициализируется после этого). Это ситуация, которая часто случается в реальных компонентах, когда написанная спецификация для подобной функцииmy_read
не на 100% ясна или даже ошибочна в отношении поведения в случае ошибки. Однако, поскольку longbytes_read
инициализируется до нуля перед вызовом, программа ведет себя так же, как если бы инициализация была выполнена внутриmy_read
, поэтому она работает правильно, в этой комбинации нет ошибки в программе.Таким образом, моя рекомендация, которая следует из этого: использовать подход без инициализации, только если
Это условия, которые вы обычно можете разместить в тестовом коде для конкретной инструментальной среды.
В производственном коде, однако, лучше всегда инициализировать такую переменную заранее, это более защитный подход, который предотвращает ошибки в случае, если контракт является неполным или неправильным, или если дезинфицирующее устройство адреса или подобные меры безопасности не активированы. И, как вы правильно написали, правило «раннего сбоя» применяется, если при выполнении программы возникает ошибка. Но если предварительная инициализация переменной означает, что в этом нет ничего плохого, нет необходимости останавливать дальнейшее выполнение.
источник
Всегда инициализируйте свои переменные
Разница между ситуациями, которые вы рассматриваете, состоит в том, что случай без инициализации приводит к неопределенному поведению , в то время как случай, когда вы потратили время на инициализацию, создает хорошо определенную и детерминированную ошибку. Я не могу не подчеркнуть, насколько сильно различаются эти два случая.
Рассмотрим гипотетический пример, который мог случиться с гипотетическим сотрудником в программе гипотетического моделирования. Эта гипотетическая команда гипотетически пыталась провести детерминистическое моделирование, чтобы продемонстрировать, что продукт, который они гипотетически продавали, соответствовал потребностям.
Хорошо, я остановлюсь на слове инъекции. Я думаю, вы поняли ;-)
В этой симуляции были сотни неинициализированных переменных. Один разработчик запустил valgrind при моделировании и заметил, что было несколько ошибок «ветвления по неинициализированным значениям». «Хм, похоже, что это может привести к недетерминированности, что затруднит повторение тестовых прогонов, когда нам это нужно больше всего». Разработчик обратился к руководству, но руководство работало в очень сжатые сроки и не могло выделить ресурсы для отслеживания этой проблемы. «Мы заканчиваем тем, что инициализируем все наши переменные, прежде чем использовать их. У нас есть хорошие методы кодирования».
За несколько месяцев до окончательной сдачи, когда симуляция находится в режиме полного оттока, и вся команда спешит завершить все то, что обещал менеджмент в рамках бюджета, который, как и любой когда-либо финансируемый проект, был слишком мал. Кто-то заметил, что они не смогли протестировать существенную функцию, потому что по какой-то причине детерминированный сим не вел себя детерминистически при отладке.
Возможно, вся команда была остановлена и потратила большую часть двух месяцев на то, чтобы прочесать всю кодовую базу симуляции, исправляя ошибки неинициализированных значений вместо реализации и тестирования функций. Само собой разумеется, что сотрудник пропустил «Я тебе так сказал» и сразу же помог другим разработчикам понять, что такое неинициализированные значения. Как ни странно, вскоре после этого инцидента стандарты кодирования были изменены, что побудило разработчиков всегда инициализировать свои переменные.
И это предупредительный выстрел. Это пуля, которая попала тебе в нос. Фактическая проблема намного, намного более коварная, чем вы можете себе представить.
Использование неинициализированного значения - это «неопределенное поведение» (за исключением нескольких угловых случаев, таких как
char
). Неопределенное поведение (или UB для краткости) настолько безумно и совершенно вредно для вас, что вы никогда не должны думать, что оно лучше, чем альтернатива. Иногда вы можете определить, что ваш конкретный компилятор определяет UB, а затем его безопасно использовать, но в противном случае неопределенное поведение - это «любое поведение, которое чувствует компилятор». Он может делать то, что вы бы назвали «нормальным», например, иметь неопределенное значение. Он может выдавать недействительные коды операций, что может привести к повреждению вашей программы. Это может вызвать предупреждение во время компиляции, или компилятор может даже считать это ошибкой.Или это может вообще ничего не делать
Моя канарейка в угольной шахте для UB - это пример движка SQL, о котором я читал. Простите, что не связал это, я не смог найти статью снова. Возникла проблема переполнения буфера в механизме SQL, когда вы передавали больший размер буфера в функцию, но только в определенной версии Debian. Ошибка была должным образом зарегистрирована и исследована. Самое смешное было: переполнение буфера было проверено . Был код для обработки переполнения буфера на месте. Это выглядело примерно так:
Я добавил больше комментариев в свое представление, но идея та же. Если
put + dataLength
обернуть, он будет меньше, чемput
указатель (для любопытных у них были проверки времени компиляции, чтобы убедиться, что unsigned int был размером указателя). Если это произойдет, мы знаем, что стандартные алгоритмы кольцевого буфера могут запутаться из-за этого переполнения, поэтому мы возвращаем 0. Или мы?Как оказалось, переполнение указателей в C ++ не определено. Поскольку большинство компиляторов обрабатывают указатели как целые числа, мы получаем типичное поведение переполнения целых чисел, которое соответствует желаемому поведению. Тем не менее, это является неопределенным поведением, то есть компилятор имеет право делать что - либо она хочет.
В случае этой ошибки, Debian была выбрать , чтобы использовать новую версию GCC , что ни один из других основных вкусов Linux не обновил в своих производственных выпусках. Эта новая версия gcc имела более агрессивный оптимизатор мертвого кода. Компилятор увидел неопределенное поведение и решил, что результатом
if
утверждения будет «все, что делает оптимизацию кода лучшим», что является абсолютно законным переводом UB. Соответственно, он сделал предположение, что, поскольку без переполнения указателя UBptr+dataLength
никогда не будет нижеptr
,if
оператор никогда не сработает и оптимизирует проверку переполнения буфера.Использование «вменяемого» UB фактически привело к тому, что у основного продукта SQL была уязвимость , связанная с переполнением буфера, которую он написал, чтобы избежать!
Никогда не полагайтесь на неопределенное поведение. Когда-либо.
источник
bool
Это отличный пример, где есть очевидные проблемы, но они появляются в других местах, если только вы не предполагаете, что работаете на очень полезной платформе, такой как x86, ARM или MIPS, где все эти проблемы решаются во время кода операции.switch
, меньше 8 из-за размеров целочисленной арифметики, поэтому они могут использовать быстрые инструкции, которые предполагают, что не существует риска появления «большого» значения. Внезапно Появляется неопределенное значение (которое никогда не может быть построено с использованием правил компилятора), что делает что-то неожиданное, и внезапно вы получаете огромный скачок с конца таблицы переходов. Разрешение неопределенных результатов здесь означает, что каждый оператор switch в программе должен иметь дополнительные ловушки для поддержки этих случаев, которые могут «никогда не произойти».Я в основном работаю на функциональном языке программирования, где вам не разрешено переназначать переменные. Когда-либо. Это полностью устраняет этот класс ошибок. Поначалу это казалось огромным ограничением, но оно вынуждает вас структурировать код так, чтобы он соответствовал порядку, в котором вы изучаете новые данные, что упрощает ваш код и облегчает его обслуживание.
Эти привычки можно перенести и на императивные языки. Почти всегда можно реорганизовать ваш код, чтобы избежать инициализации переменной фиктивным значением. Это то, что вам говорят эти рекомендации. Они хотят, чтобы вы вложили в них что-то значимое, а не то, что порадует автоматизированные инструменты.
Ваш пример с API в стиле C немного сложнее. В тех случаях, когда я использую функцию, я инициализируюсь на ноль, чтобы компилятор не жаловался, но один раз в
my_read
модульных тестах я инициализируюсь на что-то другое, чтобы убедиться, что условие ошибки работает правильно. Вам не нужно проверять каждое возможное состояние ошибки при каждом использовании.источник
Нет, это не скрывает ошибок. Вместо этого он делает поведение детерминированным таким образом, что если пользователь сталкивается с ошибкой, разработчик может воспроизвести ее.
источник
TL; DR: Есть два способа сделать эту программу правильной: инициализация переменных и молитва. Только один дает результаты последовательно.
Прежде чем я смогу ответить на ваш вопрос, мне нужно сначала объяснить, что означает неопределенное поведение . На самом деле, я позволю автору компилятора выполнить большую часть работы:
Если вы не хотите читать эти статьи, TL; DR это:
К сожалению, архетип «Демоны летят из твоего носа» совершенно не в состоянии передать последствия этого факта. В то время как предполагалось доказать, что что-нибудь могло произойти, это было настолько невероятно, что это в основном игнорировалось.
Правда в том, что Undefined Behavior влияет на саму компиляцию задолго до того, как вы даже попытаетесь использовать программу (с инструментами или без, в отладчике или нет) и может полностью изменить ее поведение.
Я нахожу пример в части 2 выше поразительным:
превращается в:
потому что очевидно, что этого
P
не может быть,0
поскольку он разыменовывается перед проверкой.Как это относится к вашему примеру?
Что ж, вы сделали распространенную ошибку, предполагая, что неопределенное поведение вызовет ошибку во время выполнения. Может и нет.
Давайте представим, что определение
my_read
это:и продолжайте, как и ожидалось, от хорошего компилятора с встраиванием:
Затем, как и ожидалось от хорошего компилятора, мы оптимизируем ненужные ветки:
bytes_read
будет использоваться неинициализированным, если быresult
не0
result
никогда не будет0
!Так
result
никогда0
:О,
result
никогда не используетсяО, мы можем отложить объявление
bytes_read
:И вот мы, строго подтверждающее преобразование оригинала, и никакой отладчик не будет перехватывать неинициализированную переменную, потому что ее нет.
Я шел по этому пути, понимая проблему, когда ожидаемое поведение и сборка не совпадают, на самом деле не весело.
источник
Давайте внимательнее посмотрим на ваш пример кода:
Это хороший пример. Если мы ожидаем такую ошибку, мы можем вставить строку
assert(bytes_read > 0);
и отловить эту ошибку во время выполнения, что невозможно с неинициализированной переменной.Но предположим, что нет, и мы находим ошибку внутри функции
use(buffer)
. Мы загружаем программу в отладчике, проверяем обратную трассировку и выясняем, что она была вызвана из этого кода. Поэтому мы ставим точку останова в верхней части этого фрагмента, запускаем снова и воспроизводим ошибку. Мы пошагово пытаемся его поймать.Если мы не инициализировали
bytes_read
, он содержит мусор. Это не обязательно содержит один и тот же мусор каждый раз. Мы переступаем чертуmy_read(buffer, &bytes_read);
. Теперь, если это значение отличается от предыдущего, мы можем вообще не воспроизвести нашу ошибку! Это может сработать в следующий раз, на том же входе, совершенно случайно. Если оно постоянно равно нулю, мы получаем последовательное поведение.Мы проверяем значение, возможно, даже на обратной трассировке в том же прогоне. Если это ноль, мы можем видеть , что что-то не так;
bytes_read
не должно быть ноль на успех. (Или, если это возможно, мы могли бы захотеть инициализировать его как -1.) Вероятно, мы можем поймать ошибку здесь. Еслиbytes_read
это правдоподобная ценность, хотя, это, случается, неправильно, мы увидели бы это сразу?Это особенно верно в отношении указателей: указатель NULL всегда будет очевиден в отладчике, его можно очень легко протестировать, и он может вызвать ошибку на современном оборудовании, если мы попытаемся разыменовать его. Указатель мусора может позже привести к невоспроизводимым ошибкам повреждения памяти, и их почти невозможно отладить.
источник
ОП не полагается на неопределенное поведение или, по крайней мере, не совсем так. Действительно, полагаться на неопределенное поведение плохо. В то же время поведение программы в неожиданном случае также не определено, но не определено иное. Если вы установите переменную в ноль, но вы не намерены иметь путь выполнения , который использует , что начальный ноль, будет ваша программа вести себя здраво , когда у вас есть ошибка и делать есть такой путь? Ты сейчас в сорняках; вы не планировали использовать это значение, но вы все равно используете его. Возможно, это будет безвредно, или, может быть, это приведет к сбою программы, или, может быть, это приведет к тому, что программа будет молча повреждать данные. Вы не знаете
ОП говорит, что есть инструменты, которые помогут вам найти эту ошибку, если вы позволите им. Если вы не инициализируете значение, но в любом случае используете его, существуют статические и динамические анализаторы, которые сообщат вам об ошибке. Статический анализатор сообщит вам еще до того, как вы начнете тестировать программу. Если, с другой стороны, вы слепо инициализируете значение, анализаторы не могут сказать, что вы не планировали использовать это начальное значение, и поэтому ваша ошибка остается незамеченной. Если вам повезло, это безвредно или просто вылетает из программы; если вам не повезло, это молча портит данные.
Единственное место, где я не согласен с ОП, - это в самом конце, где он говорит: «В противном случае он уже получит ошибку сегментации». Действительно, неинициализированная переменная не будет надежно давать ошибку сегментации. Вместо этого я бы сказал, что вы должны использовать инструменты статического анализа, которые не позволят вам даже попытаться выполнить программу.
источник
Ответ на ваш вопрос должен быть разбит на различные типы переменных, которые появляются внутри программы:
Локальные переменные
Обычно объявление должно быть прямо в том месте, где переменная сначала получает свое значение. Не объявляйте переменные как в старом стиле C:
Это устраняет 99% необходимости инициализации, окончательные значения переменных имеют значение сразу после выключения. В нескольких исключениях инициализация зависит от некоторого условия:
Я считаю, что это хорошая идея, чтобы написать эти случаи, как это:
И. е. Явно утверждаю, что выполняется некоторая разумная инициализация вашей переменной.
Переменные-члены
Здесь я согласен с тем, что сказали другие ответчики: они всегда должны быть инициализированы списками конструкторов / инициализаторов. В противном случае вам трудно обеспечить согласованность между вашими членами. И если у вас есть набор членов, который, по-видимому, не требует инициализации во всех случаях, выполните рефакторинг вашего класса, добавив эти члены в производный класс, где они всегда необходимы.
Буферы
Здесь я не согласен с другими ответами. Когда люди начинают религиозно относиться к инициализации переменных, они часто заканчивают инициализацию буферов следующим образом:
Я считаю, что это почти всегда вредно: единственный эффект этих инициализаций состоит в том, что они делают инструменты
valgrind
бесполезными. Любой код, который читает из инициализированных буферов больше, чем следовало бы, скорее всего является ошибкой. Но при инициализации эта ошибка не может быть обнаруженаvalgrind
. Так что не используйте их, если вы действительно не полагаетесь на заполнение памяти нулями (и в этом случае оставьте комментарий, говорящий о том, для чего вам нужны нули).Я также настоятельно рекомендую добавить цель в вашу систему сборки, которая запускает весь набор тестов
valgrind
или аналогичный инструмент, чтобы выявить ошибки использования перед инициализацией и утечки памяти. Это более ценно, чем все преинициализации переменных. Этаvalgrind
цель должна выполняться на регулярной основе, что наиболее важно, прежде чем любой код станет общедоступным.Глобальные переменные
Вы не можете иметь глобальные переменные, которые не инициализированы (по крайней мере, в C / C ++ и т. Д.), Поэтому убедитесь, что эта инициализация - то, что вы хотите.
источник
Base& b = foo() ? new Derived1 : new Derived2;
Base &b = base_factory(which);
. Это наиболее полезно, если вам нужно вызывать код более одного раза или если это позволяет сделать результат постоянным.?:
PITA, а заводская функция все еще излишня. Эти случаи редки, но они существуют.Приличный компилятор C, C ++ или Objective-C с правильными настройками компилятора сообщит вам во время компиляции, используется ли переменная до того, как будет установлено ее значение. Поскольку в этих языках использование значения неинициализированной переменной является неопределенным поведением, «установка значения перед использованием» не является ни подсказкой, ни рекомендацией, ни хорошей практикой, это требование 100%; в противном случае ваша программа абсолютно не работает. В других языках, таких как Java и Swift, компилятор никогда не позволит вам использовать переменную до ее инициализации.
Существует логическая разница между «инициализировать» и «установить значение». Если я хочу найти курс обмена между долларами и евро, напишите «double rate = 0.0;» тогда для переменной задано значение, но оно не инициализировано. Сохраненные здесь 0,0 не имеют ничего общего с правильным результатом. В этой ситуации, если из-за ошибки вы никогда не сохраните правильный коэффициент конверсии, у компилятора не будет возможности сообщить вам об этом. Если вы только что написали «двойная ставка»; и никогда не сохранял значимый коэффициент конверсии, сказал бы вам компилятор.
Итак: не инициализируйте переменную только потому, что компилятор говорит вам, что она используется без инициализации. Это скрывает ошибку. Реальная проблема заключается в том, что вы используете переменную, которую вы не должны использовать, или что в одном пути кода вы не установили значение. Исправьте проблему, не скрывайте ее.
Не инициализируйте переменную только потому, что компилятор может сказать вам, что она используется без инициализации. Опять вы скрываете проблемы.
Объявите переменные, близкие к использованию. Это повышает вероятность того, что вы можете инициализировать его значимым значением в точке объявления.
Избегайте повторного использования переменных. При повторном использовании переменной она, скорее всего, инициализируется бесполезным значением, когда вы используете ее для второй цели.
Было отмечено, что некоторые компиляторы имеют ложные отрицания, и что проверка на инициализацию эквивалентна проблеме остановки. Оба на практике не имеют значения. Если компилятор, как указано, не может найти использование неинициализированной переменной через десять лет после сообщения об ошибке, то пришло время искать альтернативный компилятор. Java реализует это дважды; один раз в компиляторе, один раз в верификаторе, без проблем. Простой способ обойти проблему остановки состоит не в том, чтобы требовать инициализации переменной перед ее использованием, а в том, что она инициализируется перед использованием способом, который можно проверить простым и быстрым алгоритмом.
источник