Если член данных `B` использует другой член данных `A`, объявляйте `A` перед `B`
Причина
Члены данных инициализируются в порядке их объявления и уничтожаются в обратном порядке.
Обсуждение
Если член данных B использует другой член данных A, то A должен быть объявлен перед B, чтобы A переживал B, то есть время жизни A начинается раньше и заканчивается позже времени жизни B. В противном случае при конструировании и уничтожении B будет пытаться использовать A вне его времени жизни.
Пример (плохой)
// Плохо: b использует a, но a объявлен после b.
// Порядок конструирования: b затем a; порядок уничтожения: a затем b.
// Поэтому b обращается к a вне его времени жизни.
class X {
struct B {
string* p;
explicit B(string& a) : p{&a} {}
~B() { cout << *p; } // использует a (через p)
};
B b; // конструируется первым
string a = "some heap allocated string value"; // конструируется после b; уничтожается до b
public:
X() : b{a} {} // использует a до его конструирования -> UB (use-before-alloc)
~X() = default; // обращается к a после его уничтожения -> UB (use-after-free)
};
Пример (хороший)
// Исправлено: просто объявляем a перед b
class X {
struct B {
string* p;
explicit B(string& a) : p{&a} {}
~B() { cout << *p; } // использует a (через p)
};
string a = "some heap allocated string value"; // конструируется до b; уничтожается после b
B b; // конструируется вторым
public:
X() : b{a} {} // ok
~X() = default; // ok
};
Пример (плохой)
Это также может возникнуть при конкурентном выполнении. Убедитесь, что асинхронная операция, обращающаяся к значению, завершена до уничтожения этого значения.
class X {
public:
X()
: a{std::make_unique<int>(12)}
{
b = std::make_unique<std::jthread>(
[this]{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Value: " << *a << std::endl;
});
}
std::unique_ptr<std::jthread> b;
std::unique_ptr<int> a;
};
Пример (хороший)
Это также может возникнуть при конкурентном выполнении. Убедитесь, что асинхронная операция, обращающаяся к значению, завершена до уничтожения этого значения.
// Исправлено: просто объявляем a перед b
class X {
public:
X()
: a{std::make_unique<int>(12)}
{
b = std::make_unique<std::jthread>(
[this]{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Value: " << *a << std::endl;
});
}
std::unique_ptr<int> a;
std::unique_ptr<std::jthread> b;
};
Контроль
- Помечать инициализатор члена, который ссылается на объект до его конструирования.
C.ctor: Конструкторы, присваивание и деструкторы
Эти функции управляют жизненным циклом объектов: созданием, копированием, перемещением и уничтожением. Определяйте конструкторы для гарантии и упрощения инициализации классов.
Это операции по умолчанию:
- конструктор по умолчанию:
X() - конструктор копирования:
X(const X&) - оператор копирующего присваивания:
operator=(const X&) - конструктор перемещения:
X(X&&) - оператор перемещающего присваивания:
operator=(X&&) - деструктор:
~X()
По умолчанию компилятор определяет каждую из этих операций при её использовании, но определение по умолчанию может быть подавлено.
Операции по умолчанию — это набор связанных операций, которые вместе реализуют семантику жизненного цикла объекта. По умолчанию C++ рассматривает классы как типы-значения, но не все типы являются значениями.
Правила для набора операций по умолчанию:
- C.20: Если можно избежать определения операций по умолчанию — избегайте
- C.21: Если вы определяете или
=deleteкакую-либо функцию копирования, перемещения или деструктора — определите или=deleteих все - C.22: Делайте операции по умолчанию согласованными
Правила для деструкторов:
- C.30: Определяйте деструктор, если классу требуется явное действие при уничтожении объекта
- C.31: Все ресурсы, захваченные классом, должны быть освобождены деструктором класса
- C.32: Если класс имеет сырой указатель (
T*) или ссылку (T&), рассмотрите, может ли он быть владельцем - C.33: Если класс имеет владеющий указатель-член, определите деструктор
- C.35: Деструктор базового класса должен быть либо публичным и виртуальным, либо защищённым и невиртуальным
- C.36: Деструктор не должен завершаться с ошибкой
- C.37: Делайте деструкторы
noexcept
Правила для конструкторов:
- C.40: Определяйте конструктор, если класс имеет инвариант
- C.41: Конструктор должен создавать полностью инициализированный объект
- C.42: Если конструктор не может создать корректный объект, выбрасывайте исключение
- C.43: Убедитесь, что копируемый класс имеет конструктор по умолчанию
- C.44: Предпочитайте, чтобы конструкторы по умолчанию были простыми и не выбрасывали исключения
- C.45: Не определяйте конструктор по умолчанию, который только инициализирует члены данных; вместо этого используйте инициализаторы членов
- C.46: По умолчанию объявляйте конструкторы с одним аргументом как
explicit - C.47: Определяйте и инициализируйте члены данных в порядке их объявления
- C.48: Предпочитайте инициализаторы членов по умолчанию инициализаторам членов в конструкторах для константных инициализаторов
- C.49: Предпочитайте инициализацию присваиванию в конструкторах
- C.50: Используйте фабричную функцию, если вам нужно «виртуальное поведение» при инициализации
- C.51: Используйте делегирующие конструкторы для представления общих действий для всех конструкторов класса
- C.52: Используйте наследующие конструкторы для импорта конструкторов в производный класс, не требующий дополнительной явной инициализации
Правила для копирования и перемещения:
- C.60: Делайте оператор копирующего присваивания не-
virtual, принимайте параметр поconst&и возвращайте по не-const& - C.61: Операция копирования должна копировать
- C.62: Делайте оператор копирующего присваивания безопасным для самоприсваивания
- C.63: Делайте оператор перемещающего присваивания не-
virtual, принимайте параметр по&&и возвращайте по не-const& - C.64: Операция перемещения должна перемещать и оставлять источник в корректном состоянии
- C.65: Делайте оператор перемещающего присваивания безопасным для самоприсваивания
- C.66: Делайте операции перемещения
noexcept - C.67: Полиморфный класс должен подавлять публичное копирование/перемещение
Прочие правила для операций по умолчанию:
- C.80: Используйте
=default, если нужно явно указать использование семантики по умолчанию - C.81: Используйте
=delete, когда хотите отключить поведение по умолчанию (без предоставления альтернативы) - C.82: Не вызывайте виртуальные функции в конструкторах и деструкторах
- C.83: Для типов-значений рассмотрите предоставление функции
noexcept swap - C.84:
swapне должен завершаться с ошибкой - C.85: Делайте
swapnoexcept - C.86: Делайте
==симметричным по отношению к типам операндов иnoexcept - C.87: Остерегайтесь
==для базовых классов - C.89: Делайте
hashnoexcept - C.90: Полагайтесь на конструкторы и операторы присваивания, а не на memset и memcpy
C.defop: Операции по умолчанию
По умолчанию язык предоставляет операции по умолчанию с их семантикой по умолчанию. Однако программист может отключить или заменить эти значения по умолчанию.