Constructors, Destructors, RAII
Example
Section titled “Example”// Файл 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"
// ... 15The HeapInt class above is a wrapper around a dynamically allocated int.
Unlike a regular int*, this type does not allow nullptr as a normal value.
The exception is a moved-from object. A move constructor or move-assignment
operator can leave such an object with heapValue == nullptr, preventing the same memory from being freed twice.
A moved-from object must still be valid for destruction and reassignment, but its value cannot be read
before it is assigned a new value.
The class follows RAII: it allocates memory in its constructor and deletes it in its destructor.
-
Why is the
heapValuefield (2) private?Hint
Data encapsulation.
Answer
So that code outside the class cannot overwrite the pointer directly. Only the value it points to can be overwritten from outside the class.
-
What does the syntax at (4) mean? How can it be written in the constructor body?
Answer
inline HeapInt(int value){// : heapValue(new int{value})// Выделение памяти// new intint* t = new int;// Инициализация объекта в динамической памяти// {value}*t = value;// Присваивание поля// : heapValue(...)this->heapValue = t;} -
How can this be compiled with GCC?
Answer
You can also add
-Wflags.g++ -c main.cpp -o main.og++ -c HeapInt.cpp -o HeapInt.og++ main.o HeapInt.o -o programOr with one command:
g++ main.cpp HeapInt.cpp -o program -
Why is (1) needed? What situation does it prevent?
Answer
To ensure that the header is included only once.
If another header,
demo.h, contained an#include "HeapInt.h"directive, and both headers were included inmain.cpp, then without#pragma once,HeapInt.hwould be included 2 times, causing the type and functions to be defined 2 times, which is not allowed. Therefore, the program would not compile.To prevent such bugs, which are often hard to spot,
#pragma onceis always used in headers. -
Why is (3) defined as
inline?Answer
It is defined in the header. If it is not made
inline, the linker will report an error if the header is included in more than onecompilation unit(the function is defined more than once). -
How can the requirement for a constructor or function to be
inlinebe avoided?Answer
Leave only the declaration in the header. Define it in a cpp file.
-
Why is the check at (6) necessary if
heapValueis normally notnullptr?Hint
When an object is passed to an rvalue-reference parameter (
HeapInt&&), its allocated memory can be taken from it. Code that takes ownership of this memory must also set the passed object’s pointer tonullptr.Detail
One might conclude that, since every
HeapIntobject has a non-nullheapValue, there is no need to take memory from an object passed asHeapInt&&; instead, the value could simply be copied, because the memory will be deleted in the source object’s destructor. However, for theswaptask to work correctly, it is important to transfer ownership of the memory itself—at least when the destination object’sheapValue == nullptr. Moving another object into one that was previously moved from is a valid operation.This check remains necessary because a move constructor exists.
-
Explain which constructors or overloaded operators are called at (8-14). Which of them are equivalent?
-
How can the syntax in (11-12) be disallowed?
Hint
explicit.
-
Define the constructors and operators declared in (7) at (15).
How can a constructor or overloaded operator be defined outside the class?
class Hello{Hello(AnyParam p);}Hello::Hello(AnyParam p){// ...}For an operator, the same syntax is used as for methods (the method name is
operator=).What should they do?
Remember that all these operations must comply with the rules of RAII: each block of allocated memory must have only one owner. They must work correctly in any sequence.
The copy constructor must allocate fresh memory and copy the numeric value from the source object (you can simply call the constructor that takes an
intparameter with the source object’s value).The move constructor must take the pointer from the source object and set the source object’s pointer to
nullptr. This prevents the same pointer from being deleted a second time in the destructor.The assignment operator must copy the value pointed to by the other object’s
heapValueinto the existingheapValue.The assignment operator that takes an rvalue-reference parameter must take ownership of the pointer from the source object.
-
In what other situation is the rvalue-reference overload of a constructor or assignment operator called?
Overload?
An overload is a function with the same name but different parameters.
Answer
It is called with temporary objects—that is, values of the required object type that are not stored in a variable but are passed directly to a function.
-
How many times is the
HeapIntdestructor called after (14)?Answer
As many times as there are variables of this type.
For example,
awas moved from usingstd::move, but its destructor is still called. -
Explain what you think motivated the decision not to define a default constructor for this type.
Definition
A class contract is a set of rules that always hold while an object exists, regardless of the operations that have been performed on it.
For this class, the contract is that
heapValuecannot be equal tonullptr, except for the explicitly specified state of an object after it has been moved from.More broadly, a contract describes requirements for input data (preconditions) and guarantees about output data (postconditions); it may be stated in a function’s interface. A class contract is essentially a set of conditions associated with the implicit
thisparameter that every method of the class must uphold.Answer
To uphold the class contract.
There were several options:
-
make a default constructor that would put
nullptrinheapValue, which would violate the class contract; -
allocate memory in the default constructor, which I usually do not recommend (default constructors should be cheap);
-
not provide a default constructor at all.
-
-
Explain why reading an object’s value after moving from it using
std::moveis an invalid operation.Answer
The object may have entered a state in which its value cannot be read.
For example,
heapValuemay become equal tonullptr; this is the only situation in which that can happen. -
Create a function
void swap(HeapInt& a, HeapInt& b)that swaps the values without allocating new dynamic memory.Hint 1
An implementation for a regular type that does not use RAII would be:
void swap(Whatever& a, Whatever& b){Whatever temp = a;a = b;b = temp;}Hint 2
Move operations allow a value to be transferred from one object to another.
Hint 3
Think about which operations are used in Hint 1 (the copy constructor and assignment operator). Replace them with move operations.