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

147 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Конструктор по умолчанию и = default
Гайдлайны по современному C++ всячески намекают, а иногда напрямую советуют: [следуйте "правилу нуля" (rule of zero)](https://en.cppreference.com/w/cpp/language/rule_of_three) для ваших классов, и структур и будет вам счастье! Используйте инициализаторы по умолчанию! C++20 улучшил поддержку структур-аггрегатов, так что не надо писать вручную конструкторы там где это не надо... Но legacy код существует, его затратно переписывать... А также существуют legacy разработчики, которые застряли в C++98...
Так что в старых кодовых базах можно встретить что-нибудь такое:
```C++
// 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++ -- невероятно сложная тема из-за обилия терминологии, переопределенного синтаксиса и вариативности, чтоб удовлетворить все мыслимые и немыслимые возможности. А также из-за множества особых случаев и исключений. И с подобным устаревшим подходом к описанию конструкторов как раз связано одно из таких исключений.
Пусть нам все-таки очень нужно иметь конструкторы для точки
И мы их определили в составе объявления класса
```C++
class Point2D {
public:
Point2D(int _x, int _y) : x {_x }, y {_y} {}
// Раз добавили какой-то конструктор,
// нужно добавить и конструктор по умолчанию
Point2D() = default;
int x;
int y;
};
```
И мы создаем точку, инициализированную по умолчанию
с помощью фигурных скобок, как рекомендуется в современном C++
```C++
int main() {
Point2D a {};
return a.x;
}
```
Стандарт гарантирует, что произойдет zero initialization.
Потому как в классе из тривиальных типов без инициализаторов `Point2D() = default` определил тривиальный конструктор по умолчанию. Так что все здорово. Никаких неинициализированных полей.
Но стоит нам вынести определение конструктора по умолчанию за пределы объявления класса
```C++
class Point2D {
public:
Point2D(int _x, int _y) : x {_x }, y {_y} {}
Point2D();
int x;
int y;
};
Point2D::Point2D() = default;
```
Как все резко поменяется! Теперь это уже нетривиальный конструктор. А значит инициализация фигурными скобками должна вызвать его вместо zero initialization.
И поля `x`, `y` останутся неинициализированными. Ведь мы их не инициализировали.
```C++
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 предупреждений [не выдают](https://godbolt.org/z/z1v3bEPEq).
Ну хорошо. Класс для 2D точки это все-таки отличный кандидат, чтоб просто использовать аггрегаты и списки инициализации и не думать.
Да. Делайте так!
```C++
struct Point2D {
int x = 0;
int y = 0;
};
```
Я также встречал эту проблему и в более сложных случаях:
Был класс для логгирования:
```C++
class Logger {
public:
Logger(std::string log_group)
Logger(); // определен как Logger::Logger() = default в .cpp файле
private:
// Это поле было в классе давно. У строк есть конструктор по умолчанию
// инициализатор не обязателен
std::string log_group;
};
```
В какой-то момент было решено добавить поле для контроля максимальной длины строки
```C++
class Logger {
public:
Logger(std::string log_group, size_t limit)
Logger();
private:
std::string log_group;
size_t limit; // Неопытный программист,
// которому поручили задачу, по аналогии добавил поле без инициализатора
};
```
Все компилируется, но логгер по умолчанию перестает работать, а `= default` сбивает программиста с толку.
Инициализируйте поля явно! Всегда, кроме случаев, когда инициализация действительно становится проблемой для производительности.