Skip to content

Latest commit

 

History

History
146 lines (118 loc) · 6.83 KB

default_default_constructor.md

File metadata and controls

146 lines (118 loc) · 6.83 KB

Конструктор по умолчанию и = default

Гайдлайны по современному C++ всячески намекают, а иногда напрямую советуют: следуюйте "правилу нуля" (rule of zero) для ваших классов, и структур и будет вам счастье! Используйте инициализацторы по умолчанию! C++20 улучшил поддержку структур-аггрегатов, так что не надо писать вручную конструкторы там где это не надо... Но legacy код существут, его затратно переписывать... А также существуют legacy разработчики, которые застряли в C++98...

Так что в старых кодовых базах можно встретить что-нибудь такое:

// Point.hpp
class Point2D {
public:
    Point2D(int _x, int _y);
    // Раз добавили какой-то конструктор,
    // нужно добавить и конструктор по умолчанию
    Point();

    int x;
    int y;
};

// Некоторые разработчики как мантру твердят, что 
// определение любых функций всегда нужно выносить
// в компилируемый .cpp файл. Даже коротких.
// Point.cpp
Point2D::Point2D(int _x, int _y) : x {_x}, y {_y} {}
// И даже такие!
Point::Point2D() = default;

Делать так в современном C++ крайне не рекомендуется. Не только из-за обилия бессмысленного бойлерплейта, но и из-за риска получить неинициализированные поля и неопределенное поведение вместе с ними.

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

Пусть нам все-таки очень нужно иметь конструкторы для точки

И мы их определили в составе объявления класса

class Point2D {
public:
    Point2D(int _x, int _y) : x {_x }, y {_y} {} 
    // Раз добавили какой-то конструктор,
    // нужно добавить и конструктор по умолчанию
    Point2D() = default;

    int x;
    int y;
};

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

int main() {
  
    Point2D a {};
    return a.x; 
}

Стандарт гарантирует, что произойдет zero initialization. Потому как в классе из тривиальных типов без инициализаторов Point2D() = default определил тривиальный конструктор по умолчанию. Так что все здорово. Никаких неинициализированных полей.

Но стоит нам вынести определение конструктора по умолчанию за пределы объявления класса

class Point2D {
public:
    Point2D(int _x, int _y) : x {_x }, y {_y} {} 
    Point2D();

    int x;
    int y;
};

Point2D::Point2D() = default;

Как все резко поменяется! Теперь это уже нетривиальный конструктор. А значит инициализация фигурными скобками должна вызвать его вместо zero initialization. И поля x, y останутся неинициализированными. Ведь мы их не инициализировали.

struct Bad {
    int x;
    Bad();
};
Bad::Bad() = default;

struct Good {
    int x;
    Good() = default;
};

int main() {
    Bad a {};
    Good b {};
    return a.x + b.x;
}

При компиляции GCC c -std=c++26 -O3 -Wall -Wextra -Wpedantic -Wuninitialized Мы получим предупреждение

<source>:15:14: warning: 'a.Bad::x' is used uninitialized [-Wuninitialized]
   15 |     return a.x + b.x;

Стоит отметить, что без оптимизаций, ни GCC 14, ни Clang 18 предупреждений не выдают.

Ну хорошо. Класс для 2D точки это все-таки отличный кандидат, чтоб просто использовать аггрегаты и списки инициализации и не думать.

Да. Делайте так!

struct Point2D {
    int x = 0;
    int y = 0;
};

Я также встречал эту проблему и в более сложных случаях:

Был класс для логгирования:

class Logger {
public:
    Logger(std::string log_group)
    Logger(); // определен как Logger::Logger() = default в .cpp файле
private:
    // Это поле было в классе давно. У строк есть конструктор по умолчанию
    // инициализатор не обязателен
    std::string log_group;
};

В какой-то момент было решено добавить поле для контроля максимальной длины строки

class Logger {
public:
    Logger(std::string log_group, size_t limit)
    Logger();
private:
    std::string log_group;
    size_t limit; // Неопытный программист, 
                  // которому поручили задачу, по аналогии добавил поле без инициализатора
};

Все компилируется, но логгер по умолчанию перестает работать, а = default сбивает программиста с толку.

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