mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-18 05:14:34 +03:00
* 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
147 lines
6.8 KiB
Markdown
147 lines
6.8 KiB
Markdown
# Конструктор по умолчанию и = 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` сбивает программиста с толку.
|
||
|
||
Инициализируйте поля явно! Всегда, кроме случаев, когда инициализация действительно становится проблемой для производительности.
|