Всегда инициализируйте объект
Причина
Избегайте ошибок использования до инициализации и связанного с ними неопределённого поведения. Избегайте проблем с пониманием сложной инициализации. Упрощайте рефакторинг.
Пример
void use(int arg)
{
int i; // плохо: неинициализированная переменная
// ...
i = 7; // инициализируем i
}
Нет, i = 7 не инициализирует i; это присваивание. Кроме того, i может быть прочитана в части .... Лучше:
void use(int arg) // OK
{
int i = 7; // OK: инициализировано
string s; // OK: инициализировано по умолчанию
// ...
}
Примечание
Правило всегда инициализировать намеренно строже, чем правило языка объект должен быть установлен перед использованием. Последнее, более мягкое правило, выявляет технические ошибки, но:
- Ведёт к менее читаемому коду
- Побуждает объявлять имена в излишне широких областях видимости
- Ведёт к более трудночитаемому коду
- Ведёт к логическим ошибкам, поощряя сложный код
- Затрудняет рефакторинг
Правило всегда инициализировать — это правило стиля, направленное на улучшение сопровождаемости, а также правило защиты от ошибок использования до инициализации.
Пример
Вот пример, который часто приводят как обоснование необходимости более мягкого правила инициализации:
widget i; // "widget" — тип, который дорого инициализировать, возможно, большой тривиальный тип
widget j;
if (cond) { // плохо: i и j инициализируются «поздно»
i = f1();
j = f2();
}
else {
i = f3();
j = f4();
}
Это нельзя тривиально переписать с инициализаторами для i и j. Заметьте, что для типов с конструктором по умолчанию попытка отложить инициализацию приводит просто к инициализации по умолчанию, за которой следует присваивание. Распространённая причина таких примеров — «эффективность», но компилятор, способный обнаружить использование до инициализации, также может устранить лишнюю двойную инициализацию.
Предполагая, что между i и j есть логическая связь, эту связь следует, вероятно, выразить в коде:
pair<widget, widget> make_related_widgets(bool x)
{
return (x) ? {f1(), f2()} : {f3(), f4()};
}
auto [i, j] = make_related_widgets(cond); // C++17
Если функция make_related_widgets иначе избыточна, мы можем устранить её с помощью лямбды ES.28:
auto [i, j] = [x] { return (x) ? pair{f1(), f2()} : pair{f3(), f4()} }(); // C++17
Использование значения, обозначающего «неинициализировано», — признак проблемы, а не решение:
widget i = uninit; // плохо
widget j = uninit;
// ...
use(i); // возможно, используется до инициализации
// ...
if (cond) { // плохо: i и j инициализируются «поздно»
i = f1();
j = f2();
}
else {
i = f3();
j = f4();
}
Теперь компилятор не может даже просто обнаружить использование до инициализации. Более того, мы усложнили пространство состояний для widget: какие операции допустимы над uninit widget, а какие нет?
Примечание
Сложная инициализация была популярна среди опытных программистов на протяжении десятилетий. Она также была основным источником ошибок и сложности. Многие такие ошибки вносятся при сопровождении спустя годы после первоначальной реализации.
Пример
Это правило распространяется на члены данных.
class X {
public:
X(int i, int ci) : m2{i}, cm2{ci} {}
// ...
private:
int m1 = 7;
int m2;
int m3;
const int cm1 = 7;
const int cm2;
const int cm3;
};
Компилятор укажет на неинициализированный cm3, поскольку он является const, но не обнаружит отсутствие инициализации m3. Обычно редкая лишняя инициализация члена стоит того, чтобы избежать ошибок из-за отсутствия инициализации, и зачастую оптимизатор может устранить лишнюю инициализацию (например, инициализацию, которая происходит непосредственно перед присваиванием).
Исключение
Если вы объявляете объект, который вот-вот будет инициализирован из входных данных, его инициализация привела бы к двойной инициализации. Однако остерегайтесь: это может оставить неинициализированные данные за пределами ввода — что является плодородным источником ошибок и нарушений безопасности:
constexpr int max = 8 * 1024;
int buf[max]; // OK, но подозрительно: неинициализировано
f.read(buf, max);
Стоимость инициализации этого массива может быть значительной в некоторых ситуациях. Однако такие примеры действительно оставляют неинициализированные переменные доступными, поэтому к ним следует относиться с подозрением.
constexpr int max = 8 * 1024;
int buf[max] = {}; // обнулить все элементы; лучше в некоторых ситуациях
f.read(buf, max);
Из-за ограничительных правил инициализации для массивов и std::array они предлагают наиболее убедительные примеры необходимости этого исключения.
По возможности используйте библиотечную функцию, о которой известно, что она не переполняется. Например:
string s; // s инициализируется по умолчанию как ""
cin >> s; // s расширяется для хранения строки
Не рассматривайте простые переменные, являющиеся целями операций ввода, как исключения из этого правила:
int i; // плохо
// ...
cin >> i;
В нередком случае, когда цель ввода и операция ввода разделены (что не следует делать), возникает возможность использования до инициализации.
int i2 = 0; // лучше, при условии что ноль является допустимым значением для i2
// ...
cin >> i2;
Хороший оптимизатор должен знать об операциях ввода и устранять избыточную операцию.
Примечание
Иногда лямбду можно использовать в качестве инициализатора, чтобы избежать неинициализированной переменной:
error_code ec;
Value v = [&] {
auto p = get_value(); // get_value() возвращает pair<error_code, Value>
ec = p.first;
return p.second;
}();
или, возможно:
Value v = [] {
auto p = get_value(); // get_value() возвращает pair<error_code, Value>
if (p.first) throw Bad_value{p.first};
return p.second;
}();
Смотрите также: ES.28
Контроль
- Отмечайте каждую неинициализированную переменную. Не отмечайте переменные пользовательских типов с конструкторами по умолчанию.
- Проверяйте, что в неинициализированный буфер выполняется запись сразу после объявления. Передача неинициализированной переменной в качестве неконстантного ссылочного аргумента может считаться записью в переменную.