mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
034dc9504b
* 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` сбивает программиста с толку.
|
|
|
|
Инициализируйте поля явно! Всегда, кроме случаев, когда инициализация действительно становится проблемой для производительности.
|