Разве «всегда инициализируемые переменные» не приводят к скрытию важных ошибок?

35

В 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);

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

Abyx
источник
10
Хотя я думаю, что это хороший вопрос, я не понимаю ваш пример. Если происходит ошибка чтения, и bytes_readона не изменяется (поэтому она сохраняется равной нулю), почему это должно быть ошибкой? Программа все еще может продолжаться в том же духе до тех пор, пока она не ожидает bytes_read!=0этого неявно . Так что это нормально, дезинфицирующие средства не жалуются. С другой стороны, если программа bytes_readне инициализирована заранее, программа не сможет продолжать в том же духе, поэтому отсутствие инициализации bytes_readфактически приводит к ошибке, которой раньше не было.
Док Браун
2
@Abyx: даже если он сторонний, он не работает с буфером, начиная с \0него, он глючит. Если это задокументировано, чтобы не иметь дело с этим, ваш код вызова глючит. Если вы исправите свой код вызова для проверки bytes_read==0перед вызовом use, то вы вернетесь к тому, с чего начали: ваш код глючит, если вы не инициализируете bytes_read, и безопасен, если вы это делаете. ( Обычно функции должны заполнять свои out-параметры даже в случае ошибки : не совсем. Часто выходы либо остаются одни, либо не определены.)
Mat
1
Есть ли какая-то причина, по которой этот код игнорирует err_tвозвращаемое my_read()? Если в примере есть ошибка, вот и все.
Blrfl
1
Это просто: только инициализируйте переменные, если это имеет смысл. Если это не так, не надо. Я могу согласиться с тем, что использование «фиктивных» данных для этого плохо, потому что скрывает ошибки.
Питер Б
1
«Есть еще одно правило - если при выполнении программы возникает ошибка, программа должна умереть как можно скорее. Не нужно поддерживать ее в живых, просто аварийно завершить работу, написать аварийный дамп и передать его инженерам для расследования». Попробуйте это в полете управляющее программное обеспечение. Удачи в восстановлении крушения на обломках самолета.
Джорджио

Ответы:

44

Ваши рассуждения ошибочны по нескольким причинам:

  1. Ошибки сегментации далеко не обязательно произойдут. Использование неинициализированной переменной приводит к неопределенному поведению . Ошибки сегментации - это один из способов, которым такое поведение может проявиться, но похоже, что оно работает нормально.
  2. Компиляторы никогда не заполняют неинициализированную память определенным шаблоном (например, 0xCD). Это то, что делают некоторые отладчики, чтобы помочь вам найти места, где используются неинициализированные переменные. Если вы запускаете такую ​​программу вне отладчика, то эта переменная будет содержать совершенно случайный мусор. В равной степени вероятно, что такой счетчик bytes_readимеет значение, 10как и значение 0xcdcdcdcd.
  3. Даже если вы работаете в отладчике, который устанавливает неинициализированную память в фиксированный шаблон, они делают это только при запуске. Это означает, что этот механизм надежно работает только для статических (и, возможно, распределенных в куче) переменных. Для автоматических переменных, которые размещаются в стеке или хранятся только в регистре, высока вероятность того, что переменная будет сохранена в месте, которое использовалось ранее, поэтому образец контрольной памяти уже был перезаписан.

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

  1. Переменная содержит полезное значение с самого начала своего существования. Если вы объедините это с указанием объявлять переменную только тогда, когда вам это нужно, вы можете избежать того, чтобы будущие программисты по техническому обслуживанию попали в ловушку того, чтобы начать использовать переменную между ее объявлением и первым присваиванием, где переменная будет существовать, но не будет инициализирована.

  2. Переменная содержит определенное значение, которое вы можете проверить позже, чтобы узнать, my_readобновила ли функция как значение. Без инициализации вы не сможете определить, bytes_readдействительно ли имеет допустимое значение, потому что вы не можете знать, с какого значения оно началось.

Барт ван Инген Шенау
источник
8
1) все дело в вероятностях, например, 1% против 99%. 2 и 3) VC ++ генерирует такой код инициализации, в том числе и для локальных переменных. 3) статические (глобальные) переменные всегда инициализируются с 0.
Abyx
5
@Abyx: 1) По моему опыту, вероятность составляет ~ 80% «нет очевидных различий в поведении», 10% «делает не то», 10% «сегфо». Что касается (2) и (3): VC ++ делает это только в отладочных сборках. Полагаться на это - ужасно плохая идея, поскольку она выборочно нарушает сборки релизов и не проявляется во многих ваших тестах.
Кристиан Айхингер,
8
Я думаю, что «идея за руководством» является наиболее важной частью этого ответа. Руководство абсолютно не советует вам следовать каждому объявлению переменной с = 0;. Цель совета - объявить переменную в том месте, где у вас будет для нее полезное значение, и немедленно назначить это значение. Это четко указано в следующих непосредственно правилах ES21 и ES22. Все эти три следует понимать как совместную работу; не как отдельные несвязанные правила.
GrandOpener
1
@GrandOpener Точно. Если в точке, где объявлена ​​переменная, нет значимого значения, которое можно назначить, область видимости переменной, вероятно, неверна.
Кевин Крумвиде,
5
«Компиляторы никогда не заполняются», не так ли не всегда ?
CodesInChaos
25

Вы написали, что «это правило не помогает находить ошибки, оно только скрывает их» - ну, цель правила не в том, чтобы находить ошибки, а в том, чтобы их избегать . И когда ошибка устраняется, ничего не скрывается.

Обсудим проблему с точки зрения вашего примера: предположим, что my_readфункция имеет письменный контракт на инициализацию bytes_readпри любых обстоятельствах, но это не так в случае ошибки, поэтому она, по крайней мере, в этом случае неисправна. Вы намерены использовать среду времени выполнения, чтобы показать эту ошибку, не инициализируя bytes_readпараметр в первую очередь. Если вы точно знаете, что существует дезинфицирующее средство для адресов, это действительно возможный способ обнаружить такую ​​ошибку. Чтобы исправить ошибку, нужно изменить my_readфункцию внутри.

Но есть и другая точка зрения, которая, по крайней мере, в равной степени верна: ошибочное поведение возникает только из комбинации не инициализации bytes_readзаранее и вызова my_readпосле (с ожиданием bytes_readинициализируется после этого). Это ситуация, которая часто случается в реальных компонентах, когда написанная спецификация для подобной функции my_readне на 100% ясна или даже ошибочна в отношении поведения в случае ошибки. Однако, поскольку long bytes_readинициализируется до нуля перед вызовом, программа ведет себя так же, как если бы инициализация была выполнена внутри my_read, поэтому она работает правильно, в этой комбинации нет ошибки в программе.

Таким образом, моя рекомендация, которая следует из этого: использовать подход без инициализации, только если

  • Вы хотите проверить, инициализирует ли функция или кодовый блок определенный параметр
  • вы на 100% уверены, что у рассматриваемой функции есть контракт, в котором определенно неправильно не присваивать значение этому параметру
  • Вы на 100% уверены, что окружающая среда может это поймать

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

В производственном коде, однако, лучше всегда инициализировать такую ​​переменную заранее, это более защитный подход, который предотвращает ошибки в случае, если контракт является неполным или неправильным, или если дезинфицирующее устройство адреса или подобные меры безопасности не активированы. И, как вы правильно написали, правило «раннего сбоя» применяется, если при выполнении программы возникает ошибка. Но если предварительная инициализация переменной означает, что в этом нет ничего плохого, нет необходимости останавливать дальнейшее выполнение.

Док Браун
источник
4
Это именно то, о чем я думал, когда читал это. Это не сметает вещи под ковром, это сметает их в мусорный ящик!
CorsiKa
22

Всегда инициализируйте свои переменные

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

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

Хорошо, я остановлюсь на слове инъекции. Я думаю, вы поняли ;-)

В этой симуляции были сотни неинициализированных переменных. Один разработчик запустил valgrind при моделировании и заметил, что было несколько ошибок «ветвления по неинициализированным значениям». «Хм, похоже, что это может привести к недетерминированности, что затруднит повторение тестовых прогонов, когда нам это нужно больше всего». Разработчик обратился к руководству, но руководство работало в очень сжатые сроки и не могло выделить ресурсы для отслеживания этой проблемы. «Мы заканчиваем тем, что инициализируем все наши переменные, прежде чем использовать их. У нас есть хорошие методы кодирования».

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

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

И это предупредительный выстрел. Это пуля, которая попала тебе в нос. Фактическая проблема намного, намного более коварная, чем вы можете себе представить.

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

Или это может вообще ничего не делать

Моя канарейка в угольной шахте для UB - это пример движка SQL, о котором я читал. Простите, что не связал это, я не смог найти статью снова. Возникла проблема переполнения буфера в механизме SQL, когда вы передавали больший размер буфера в функцию, но только в определенной версии Debian. Ошибка была должным образом зарегистрирована и исследована. Самое смешное было: переполнение буфера было проверено . Был код для обработки переполнения буфера на месте. Это выглядело примерно так:

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

Я добавил больше комментариев в свое представление, но идея та же. Если put + dataLengthобернуть, он будет меньше, чем putуказатель (для любопытных у них были проверки времени компиляции, чтобы убедиться, что unsigned int был размером указателя). Если это произойдет, мы знаем, что стандартные алгоритмы кольцевого буфера могут запутаться из-за этого переполнения, поэтому мы возвращаем 0. Или мы?

Как оказалось, переполнение указателей в C ++ не определено. Поскольку большинство компиляторов обрабатывают указатели как целые числа, мы получаем типичное поведение переполнения целых чисел, которое соответствует желаемому поведению. Тем не менее, это является неопределенным поведением, то есть компилятор имеет право делать что - либо она хочет.

В случае этой ошибки, Debian была выбрать , чтобы использовать новую версию GCC , что ни один из других основных вкусов Linux не обновил в своих производственных выпусках. Эта новая версия gcc имела более агрессивный оптимизатор мертвого кода. Компилятор увидел неопределенное поведение и решил, что результатом ifутверждения будет «все, что делает оптимизацию кода лучшим», что является абсолютно законным переводом UB. Соответственно, он сделал предположение, что, поскольку без переполнения указателя UB ptr+dataLengthникогда не будет ниже ptr, ifоператор никогда не сработает и оптимизирует проверку переполнения буфера.

Использование «вменяемого» UB фактически привело к тому, что у основного продукта SQL была уязвимость , связанная с переполнением буфера, которую он написал, чтобы избежать!

Никогда не полагайтесь на неопределенное поведение. Когда-либо.

Корт Аммон - Восстановить Монику
источник
Для очень забавного прочтения о неопределенном поведении, software.intel.com/en-us/blogs/2013/01/06/… удивительно хорошо написано сообщение о том, как плохо это может пойти. Тем не менее, этот конкретный пост посвящен атомарным операциям, которые для большинства очень запутаны, поэтому я не рекомендую его в качестве учебника для начинающих и как он может пойти не так.
Корт Аммон - Восстановить Монику
1
Мне бы хотелось, чтобы у C были встроенные свойства для установки lvalue или массива из них в неинициализированные, не захватывающие неопределенные значения или неуказанные значения, или превращающие неприятные lvalue в менее неприятные (не захватывающие неопределенные или неуказанные), оставляя определенные значения в покое. Компиляторы могли бы использовать такие директивы для помощи в полезной оптимизации, а программисты могли бы использовать их, чтобы избежать необходимости писать бесполезный код, в то же время блокируя ломающиеся «оптимизации» при использовании таких вещей, как методы разреженной матрицы.
суперкат
@supercat Было бы неплохо, если вы нацелены на платформы, где это правильное решение. Одним из примеров известных проблем является возможность создавать шаблоны памяти, которые не только недопустимы для типа памяти, но их невозможно достичь обычными способами. boolЭто отличный пример, где есть очевидные проблемы, но они появляются в других местах, если только вы не предполагаете, что работаете на очень полезной платформе, такой как x86, ARM или MIPS, где все эти проблемы решаются во время кода операции.
Cort Ammon - Восстановить Монику
Рассмотрим случай, когда оптимизатор может доказать, что значение, используемое для a switch, меньше 8 из-за размеров целочисленной арифметики, поэтому они могут использовать быстрые инструкции, которые предполагают, что не существует риска появления «большого» значения. Внезапно Появляется неопределенное значение (которое никогда не может быть построено с использованием правил компилятора), что делает что-то неожиданное, и внезапно вы получаете огромный скачок с конца таблицы переходов. Разрешение неопределенных результатов здесь означает, что каждый оператор switch в программе должен иметь дополнительные ловушки для поддержки этих случаев, которые могут «никогда не произойти».
Cort Ammon - Восстановить Монику
Если бы встроенные функции были стандартизированы, компиляторы могли бы делать все, что необходимо для соблюдения семантики; если, например, некоторые пути кода устанавливают переменную, а некоторые нет, а встроенная функция говорит «преобразовать в неопределенное значение, если не инициализирован или неопределен; в противном случае оставьте его в покое», компилятор для платформ с регистрами «не-значение» должен будет вставьте код, чтобы либо инициализировать переменную либо перед любыми путями кода, либо в любых путях кода, в которых инициализация в противном случае была бы пропущена, но семантический анализ, необходимый для этого, довольно прост.
суперкат
5

Я в основном работаю на функциональном языке программирования, где вам не разрешено переназначать переменные. Когда-либо. Это полностью устраняет этот класс ошибок. Поначалу это казалось огромным ограничением, но оно вынуждает вас структурировать код так, чтобы он соответствовал порядку, в котором вы изучаете новые данные, что упрощает ваш код и облегчает его обслуживание.

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

Ваш пример с API в стиле C немного сложнее. В тех случаях, когда я использую функцию, я инициализируюсь на ноль, чтобы компилятор не жаловался, но один раз в my_readмодульных тестах я инициализируюсь на что-то другое, чтобы убедиться, что условие ошибки работает правильно. Вам не нужно проверять каждое возможное состояние ошибки при каждом использовании.

Карл Билефельдт
источник
5

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


источник
1
И инициализация с -1 может быть действительно значимой. Где «int bytes_read = 0» - это плохо, потому что вы действительно можете прочитать 0 байтов, инициализация его с -1 дает понять, что ни одна попытка чтения байтов не удалась, и вы можете проверить это.
Питер Б
4

TL; DR: Есть два способа сделать эту программу правильной: инициализация переменных и молитва. Только один дает результаты последовательно.


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

Если вы не хотите читать эти статьи, TL; DR это:

Undefined Behavior - это социальный договор между разработчиком и компилятором; компилятор со слепой верой полагает, что его пользователь никогда не будет полагаться на неопределенное поведение.

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

Правда в том, что Undefined Behavior влияет на саму компиляцию задолго до того, как вы даже попытаетесь использовать программу (с инструментами или без, в отладчике или нет) и может полностью изменить ее поведение.

Я нахожу пример в части 2 выше поразительным:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

превращается в:

void contains_null_check(int *P) {
  *P = 4;
}

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


Как это относится к вашему примеру?

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.

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

Давайте представим, что определение my_readэто:

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

и продолжайте, как и ожидалось, от хорошего компилятора с встраиванием:

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

Затем, как и ожидалось от хорошего компилятора, мы оптимизируем ненужные ветки:

  1. Ни одна переменная не должна использоваться неинициализированной
  2. bytes_readбудет использоваться неинициализированным, если бы resultне0
  3. Разработчик обещает, что resultникогда не будет 0!

Так resultникогда 0:

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

О, resultникогда не используется

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

О, мы можем отложить объявление bytes_read:

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

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

Я шел по этому пути, понимая проблему, когда ожидаемое поведение и сборка не совпадают, на самом деле не весело.

Матье М.
источник
Иногда я думаю, что компиляторы должны заставить программу удалять исходные файлы при выполнении пути UB. Затем программисты узнают, что UB означает для их конечного пользователя ....
mattnz
1

Давайте внимательнее посмотрим на ваш пример кода:

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);

Это хороший пример. Если мы ожидаем такую ​​ошибку, мы можем вставить строку assert(bytes_read > 0);и отловить эту ошибку во время выполнения, что невозможно с неинициализированной переменной.

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

Если мы не инициализировали bytes_read, он содержит мусор. Это не обязательно содержит один и тот же мусор каждый раз. Мы переступаем черту my_read(buffer, &bytes_read);. Теперь, если это значение отличается от предыдущего, мы можем вообще не воспроизвести нашу ошибку! Это может сработать в следующий раз, на том же входе, совершенно случайно. Если оно постоянно равно нулю, мы получаем последовательное поведение.

Мы проверяем значение, возможно, даже на обратной трассировке в том же прогоне. Если это ноль, мы можем видеть , что что-то не так; bytes_readне должно быть ноль на успех. (Или, если это возможно, мы могли бы захотеть инициализировать его как -1.) Вероятно, мы можем поймать ошибку здесь. Если bytes_readэто правдоподобная ценность, хотя, это, случается, неправильно, мы увидели бы это сразу?

Это особенно верно в отношении указателей: указатель NULL всегда будет очевиден в отладчике, его можно очень легко протестировать, и он может вызвать ошибку на современном оборудовании, если мы попытаемся разыменовать его. Указатель мусора может позже привести к невоспроизводимым ошибкам повреждения памяти, и их почти невозможно отладить.

Davislor
источник
1

ОП не полагается на неопределенное поведение или, по крайней мере, не совсем так. Действительно, полагаться на неопределенное поведение плохо. В то же время поведение программы в неожиданном случае также не определено, но не определено иное. Если вы установите переменную в ноль, но вы не намерены иметь путь выполнения , который использует , что начальный ноль, будет ваша программа вести себя здраво , когда у вас есть ошибка и делать есть такой путь? Ты сейчас в сорняках; вы не планировали использовать это значение, но вы все равно используете его. Возможно, это будет безвредно, или, может быть, это приведет к сбою программы, или, может быть, это приведет к тому, что программа будет молча повреждать данные. Вы не знаете

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

Единственное место, где я не согласен с ОП, - это в самом конце, где он говорит: «В противном случае он уже получит ошибку сегментации». Действительно, неинициализированная переменная не будет надежно давать ошибку сегментации. Вместо этого я бы сказал, что вы должны использовать инструменты статического анализа, которые не позволят вам даже попытаться выполнить программу.

Джордан Браун
источник
0

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


Локальные переменные

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

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

Это устраняет 99% необходимости инициализации, окончательные значения переменных имеют значение сразу после выключения. В нескольких исключениях инициализация зависит от некоторого условия:

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

Я считаю, что это хорошая идея, чтобы написать эти случаи, как это:

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

И. е. Явно утверждаю, что выполняется некоторая разумная инициализация вашей переменной.


Переменные-члены

Здесь я согласен с тем, что сказали другие ответчики: они всегда должны быть инициализированы списками конструкторов / инициализаторов. В противном случае вам трудно обеспечить согласованность между вашими членами. И если у вас есть набор членов, который, по-видимому, не требует инициализации во всех случаях, выполните рефакторинг вашего класса, добавив эти члены в производный класс, где они всегда необходимы.


Буферы

Здесь я не согласен с другими ответами. Когда люди начинают религиозно относиться к инициализации переменных, они часто заканчивают инициализацию буферов следующим образом:

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

Я считаю, что это почти всегда вредно: единственный эффект этих инициализаций состоит в том, что они делают инструменты valgrindбесполезными. Любой код, который читает из инициализированных буферов больше, чем следовало бы, скорее всего является ошибкой. Но при инициализации эта ошибка не может быть обнаружена valgrind. Так что не используйте их, если вы действительно не полагаетесь на заполнение памяти нулями (и в этом случае оставьте комментарий, говорящий о том, для чего вам нужны нули).

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


Глобальные переменные

Вы не можете иметь глобальные переменные, которые не инициализированы (по крайней мере, в C / C ++ и т. Д.), Поэтому убедитесь, что эта инициализация - то, что вы хотите.

cmaster
источник
Заметьте, что вы можете написать условную инициализацию с помощью троичного оператора, например Base& b = foo() ? new Derived1 : new Derived2;
Davislor
@Lorehead Это может работать для простых случаев, но не для более сложных: вы не хотите делать это, если у вас есть три или более случаев, и ваши конструкторы принимают три или более аргументов, просто для удобства чтения причины. И это даже не рассматривает какие-либо вычисления, которые могут потребоваться, например, поиск аргумента для одной ветви инициализации в цикле.
Мастер
Для более сложных случаев, можно обернуть код инициализации функции фабричной: Base &b = base_factory(which);. Это наиболее полезно, если вам нужно вызывать код более одного раза или если это позволяет сделать результат постоянным.
Дэвислор
@Lorehead Это правда, и, безусловно, путь, если требуемая логика не проста. Тем не менее, я верю, что между ними есть небольшая серая область, где инициализация через ?:PITA, а заводская функция все еще излишня. Эти случаи редки, но они существуют.
Мастер
-2

Приличный компилятор C, C ++ или Objective-C с правильными настройками компилятора сообщит вам во время компиляции, используется ли переменная до того, как будет установлено ее значение. Поскольку в этих языках использование значения неинициализированной переменной является неопределенным поведением, «установка значения перед использованием» не является ни подсказкой, ни рекомендацией, ни хорошей практикой, это требование 100%; в противном случае ваша программа абсолютно не работает. В других языках, таких как Java и Swift, компилятор никогда не позволит вам использовать переменную до ее инициализации.

Существует логическая разница между «инициализировать» и «установить значение». Если я хочу найти курс обмена между долларами и евро, напишите «double rate = 0.0;» тогда для переменной задано значение, но оно не инициализировано. Сохраненные здесь 0,0 не имеют ничего общего с правильным результатом. В этой ситуации, если из-за ошибки вы никогда не сохраните правильный коэффициент конверсии, у компилятора не будет возможности сообщить вам об этом. Если вы только что написали «двойная ставка»; и никогда не сохранял значимый коэффициент конверсии, сказал бы вам компилятор.

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

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

Объявите переменные, близкие к использованию. Это повышает вероятность того, что вы можете инициализировать его значимым значением в точке объявления.

Избегайте повторного использования переменных. При повторном использовании переменной она, скорее всего, инициализируется бесполезным значением, когда вы используете ее для второй цели.

Было отмечено, что некоторые компиляторы имеют ложные отрицания, и что проверка на инициализацию эквивалентна проблеме остановки. Оба на практике не имеют значения. Если компилятор, как указано, не может найти использование неинициализированной переменной через десять лет после сообщения об ошибке, то пришло время искать альтернативный компилятор. Java реализует это дважды; один раз в компиляторе, один раз в верификаторе, без проблем. Простой способ обойти проблему остановки состоит не в том, чтобы требовать инициализации переменной перед ее использованием, а в том, что она инициализируется перед использованием способом, который можно проверить простым и быстрым алгоритмом.

gnasher729
источник
Это звучит внешне хорошо, но слишком сильно зависит от точности предупреждений о неинициализированных значениях. Получение этих совершенно правильно эквивалентно Проблеме Остановки, и производственные компиляторы могут и не страдать ложноотрицательные (то есть они не диагностировать неинициализированную переменные , когда они должны иметь); посмотрите, например, ошибку GCC 18501 , которая не исправлена более десяти лет назад.
zwol
То, что вы говорите о gcc, только что сказано. Остальное не имеет значения.
gnasher729
Грустно с gcc, но если вы не понимаете, почему все остальное имеет значение, вам нужно учиться.
zwol