Конструкторы, деструкторы, RAII
// Файл HeapInt.h#pragma once // 1#include <cassert>
class HeapInt{private: int* heapValue; // 2
public: // 3 inline HeapInt(int val) // 4 : heapValue(new int{ val }) { }
inline ~HeapInt() { // Заметка: delete проверяет на nullptr сам. delete heapValue; // 5 }
inline int& asRef() { assert(heapValue != nullptr); // 6 return *heapValue; }
HeapInt(const HeapInt& hello); // 7 HeapInt(HeapInt&& hello); void operator=(const HeapInt& hello); void operator=(HeapInt&& hello);};
// Файл main.cpp#include "HeapInt.h"#include <utility>
void constructors(){ HeapInt a{5}; // 8 HeapInt b{a}; // 9 HeapInt c{std::move(a)}; // 10 HeapInt d = b; // 11 HeapInt e = std::move(b); // 12 d = std::move(c); // 13 d = e; // 14}
// Файл HeapInt.cpp#include "HeapInt.h"
// ... 15Дан класс HeapInt выше, представляющий собой обертку для динамически выделенного инта.
В отличие от обычного int*, этот тип не позволяет nullptr как обычное пользовательское значение.
Исключение — объект, из которого значение было перемещено. Move-конструктор или move-оператор
присваивания может оставить такой объект с heapValue == nullptr, чтобы прежняя память
не была освобождена дважды. Перемещённый объект по-прежнему должен оставаться корректным для
уничтожения и повторного присваивания, но считывать его значение до нового присваивания нельзя.
Класс следует RAII, выделяя память в конструкторе, и удаляя ее в деструкторе.
-
Почему поле
heapValue(2) приватное?Подсказка
Инкапсуляция данных.
Ответ
Чтобы его нельзя было перезаписать извне. Наш объект позволяет перезаписывать только значение.
-
Что означает этот синтаксис (4)? Как его расписать в теле конструктора?
Ответ
inline HeapInt(int value){// : heapValue(new int{value})// Выделение памяти// new intint* t = new int;// Инициализация объекта в динамической памяти// {value}*t = value;// Присваивание поля// : heapValue(...)this->heapValue = t;} -
Как скомпилировать это с помощью GCC?
Ответ
Можно еще добавить флагов
-W.g++ -c main.cpp -o main.og++ -c HeapInt.cpp -o HeapInt.og++ main.o HeapInt.o -o programИли одной командой:
g++ main.cpp HeapInt.cpp -o program -
Зачем (1)? Какую ситуацию это предотвратит?
Ответ
Для того чтобы хедер включался только 1 раз.
Если был бы еще один хедер,
demo.h, который делает#include "HeapInt.h", и оба хедера бы включались вmain.cpp, безpragma once,HeapInt.hвключился бы 2 раза, что определит тип и функции 2 раза, что не разрешено. Поэтому программа не скомпилируется.Чтобы предотвратить подобные баги, которые часто неочевидны,
#pragma onceиcпользуют всегда в хедерах. -
Почему (3) определена как
inline?Ответ
Она определена в хедере. Если не сделать ее
inline, линкер даст ошибку, если хедер был импортирован больше чем в одномcompilation unit-е (повторное определение функции). -
Как можно обойти ограничение, что конструктор или функция должны быть
inline?Ответ
Оставить только прототип. Определить/реализовать в cpp файле.
-
Зачем на (6) нужна проверка, если
int*не может бытьnullptr?Подсказка
При передачи параметром rvalue reference (
HeapInt&&), из объекта может быть забрана выделенная им память. Тот, кто забирает эту память, должен будет также обнулить передаваемый объект.Деталь
Можно посчитать, что, так как каждый объект
HeapIntимеет ненулевойheapValue, можно и не забирать память изHeapInt&&, а просто перекопировать значение, поскольку она будет удалена в деструкторе временного объекта. Однако, для правильной работы пункта соswap, это будет важно осуществить именно путем забирания памяти, как минимум когдаheapValue == nullptr. Перемещение другого объекта в перемещенный до этого объект - это допустимая операция.Эта проверка остается необходимой, из-за существования move конструктора.
-
Объясните, какой из конструкторов или перегруженных операторов вызовется для (8-14). Какие из них эквивалентны?
-
Как запретить синтаксис на (11-12)?
Подсказка
explicit.
-
Определите на (15) конструкторы и операторы на (7).
Как определить конструктор или перегруженный оператор отдельно?
class Hello{Hello(AnyParam p);}Hello::Hello(AnyParam p){// ...}Для оператора, используется тот же синтаксис, что и для методов (имя метода будет
operator=).А что в них делать?
Помните, что все эти действия должны соблюдать правила RAII (только один объект владеет конкретной памятью). Любой порядок их выполнения должен быть поддерживаем.
Конструктор копирования должен выделять свежую память и копировать значение числа с переданного объекта (можно просто вызвать конструктор с
intпараметром, передав значение из второго объекта).Move конструктор должен забрать указатель из переданного временного объекта, выставив у него
nullptr. Выставлятьnullptrнужно для того, чтобы тот же указатель не удалился второй раз в деструкторе.Оператор присваивания должен перекопировать значение по указателю
heapValueвторого объекта в уже имеющийсяheapValue.Оператор присваивания с rvalue reference параметром должен забрать указатель из переданного объекта.
-
В какой еще ситуации вызовется rvalue reference перегрузка конструктора или оператора присваивания?
Перегрузка?
Перегрузка означает функция с тем же именем, но другими параметрами.
Ответ
Со временными объектами. То есть для значений типа ожидаемого объекта, которые не были сохранены в переменную, а переданы сразу функции.
-
Сколько раз вызовется деструктор для типа
HeapIntпосле (14)?Ответ
Столько раз, сколько переменных этого типа определено.
Например,
aбыл перемещен черезstd::move, но его деструктор все равно вызовется. -
Объясните, какая по вашему мнению была мотивация того, что у этого типа не был определен дефолтный конструктор?
Определение понятия
Контракт класса означает что-то типа правил, которые всегда выполняются при существовании объекта, вне зависимости от того, какие операции с ним были выполнены.
Для этого класса контракт заключается в том, что
heapValueне может быть равенnullptr, кроме явно оговоренного состояния объекта после перемещения.В более широком смысле слова, контракт означает требования для входных данных (preconditions) и гарантии для выходных данных (postconditions), и может упомянаться в интерфейсе функции. Контракт класса это по сути условия, которые должен выполнять каждый из методов этого класса, общий контракт для каждого метода, связанный со скрытым параметром (
this).Ответ
Для соблюдения контракта класса.
Был выбор:
-
сделать дефолтный конструктор, который бы вписывал
nullptrвheapValue, что бы нарушило контракт класса; -
выделять память в дефолтном конструкторе, что обычно делать не рекомендую (дефолтные конструкторы должны быть дешевыми);
-
не делать конструктор вообще.
-
-
Объясните, почему считывать значение объекта после применения
std::moveэто неправильная операция?Ответ
Объект может перестать быть действительным (вошел в невалидное состояние).
Например,
heapValueможет стать равенnullptr. И только в такой ситуации. -
Создайте функцию
void swap(HeapInt& a, HeapInt& b), которая меняет значения, без выделения новой динамической памяти.Подсказка 1
Имплементация для обычного типа, не использующего RAII, будет следующая:
void swap(Whatever& a, Whatever& b){Whatever temp = a;a = b;b = temp;}Подсказка 2
Move операции позволяют “выгружать” значение из одного объекта в другой.
Подсказка 3
Подумайте, какие операции бы использовались в 1 подсказке (конструктор копирования, оператор присваивания). Замените их на move операции.