Files
ubbook/syntax/default_default_constructor.md
Aleksandr 034dc9504b Много typos и небольших правок (#124)
* Update comparison_operator_rewrite.md: typos

* Update default_default_constructor.md: typos

Не уверен насчёт добавления `2D` к Point. По смыслу, вроде бы, нужно

* Update shared_from_this.md

* Update function_pass_and_address_restriction.md

* Update enable_if_void_t.md

* Update static_initialization_order_fiasco.md

* Update uninitialized.md

* Update ownership_and_exceptions.md

* Update vptr.md
2025-04-29 00:00:42 +01:00

6.8 KiB
Raw Permalink Blame History

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

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

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


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

    int x;
    int y;
};

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

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