C++ Programming
Constructors, Destructors, Copying, Moving and RAII
PGCP-AC
C++ gives objects precise lifetimes. Construction establishes a valid state, ordinary operations preserve it and destruction releases owned resources. Copy and move operations determine how one object's state is reproduced or transferred. Together, these rules make deterministic resource management possible.
1. Constructors
A constructor has the class name and no return type:
class Account {
public:
Account(std::string owner, Money opening);
private:
std::string owner_;
Money balance_;
};
Creating an Account selects a viable constructor. The constructor must establish every invariant before the object becomes available to its caller. A constructor that cannot create a valid object should reject input, commonly by throwing an exception.
Constructors can be overloaded. Overload resolution uses the supplied arguments just as it does for ordinary functions.
2. Default construction
A default constructor can be called with no arguments:
Account();
If a class declares no constructor, the compiler may declare an implicit default constructor. Once another constructor is declared, a no-argument constructor is not automatically supplied.
A default constructor should create a meaningful valid state. If no sensible default exists, omit or delete it rather than create a partly initialized object:
Account() = delete;
The defaulted form requests compiler-generated behaviour:
Account() = default;
3. Member initializer lists
Account::Account(std::string owner, Money opening)
: owner_(std::move(owner)),
balance_(opening) {
if (balance_ < Money{}) {
throw std::invalid_argument("negative opening balance");
}
}
Members are initialized before the constructor body. The initializer list constructs each member directly. Assignment inside the body would first initialize a member and then assign another value, which may be less efficient or impossible.
Reference members and const data members must be initialized. Members whose types have no default constructor also require an initializer.
4. Initialization order
Members initialize in their declaration order, not the textual order of the initializer list. Base subobjects initialize before members.
class Example {
int first_;
int second_;
public:
Example(int value)
: second_(value),
first_(second_) {}
};
Despite the written order, first_ initializes before second_, so using second_ there is wrong. Write initializer lists in declaration order and compile with reorder warnings.
After all bases and members initialize, the constructor body runs. If construction throws, already constructed subobjects are destroyed in reverse order; the incomplete object's destructor itself does not run.
5. Delegating constructors
One constructor can delegate to another constructor of the same class:
class Point {
public:
Point() : Point(0, 0) {}
Point(int x, int y) : x_(x), y_(y) {}
private:
int x_;
int y_;
};
Delegation centralizes validation and initialization. A delegating initializer must be the only member initializer in that constructor. Avoid delegation cycles because they cannot complete construction.
6. Explicit constructors
A single-argument constructor can define an implicit conversion:
class Distance {
public:
explicit Distance(double metres);
};
Explicit prevents unintended conversions such as passing a double where Distance is expected. Direct construction remains available. Multi-argument constructors can also be explicit when defaulted trailing parameters would otherwise permit conversion.
Use implicit conversion only when it is safe, unsurprising and preserves meaning.
7. Destructors
A destructor has the class name prefixed by a tilde:
class File {
public:
~File();
};
It runs when an automatic object leaves scope, a member-containing owner dies, a dynamic object is correctly deleted or another lifetime rule ends the object. A destructor takes no parameters and has no return type.
Destructors should release owned resources and should not normally allow exceptions to escape. An exception escaping during stack unwinding can terminate the program.
8. Destruction order
Automatic local objects are destroyed in reverse order of completed construction. For a class object, the destructor body runs first, followed by members in reverse declaration order, followed by base subobjects in reverse construction order.
This reverse structure is why layered ownership works. A stream depending on a file handle can be declared after the handle and will be destroyed before it.
Static-duration destruction occurs near program termination, but relative order across translation units can be difficult to control. Avoid global objects with interdependent destruction.
9. RAII
Resource Acquisition Is Initialization places resource ownership in an object. Construction acquires or receives the resource; destruction releases it.
void process(const std::filesystem::path& path) {
std::ifstream input(path);
if (!input) {
throw std::runtime_error("open failed");
}
// input closes automatically
}
Early return and exception unwinding still destroy input. RAII applies to memory, files, sockets, locks, transactions and library handles.
Every acquired resource should enter an owner immediately. This prevents gaps in which an exception can bypass cleanup.
10. Copy construction
Copy construction creates a new object from an existing object:
Record original;
Record copy = original;
The usual signature is:
Record(const Record& other);
The compiler-generated copy constructor copies each base and member. This is correct when members have correct value-copy semantics, as string and vector do.
Copy initialization, argument passing by value and some returns can request copying, though copy elision may omit the operation.
11. Copy assignment
Copy assignment replaces the state of an already existing object:
Record first;
Record second;
second = first;
Its usual signature returns the current object:
Record& operator=(const Record& other);
It must handle self-assignment and preserve validity if member copying fails. Member types with safe assignment make the generated operator preferable.
Construction creates a lifetime; assignment modifies an existing lifetime. They are different operations.
12. Shallow and deep copying
A generated copy of a raw owning pointer copies only its address:
class Buffer {
std::size_t size_;
int* data_;
};
Two copied Buffer objects would appear to own one allocation, leading to double deletion and shared mutation. A correct value copy would allocate separate storage and copy elements.
The better design is to store a vector or unique ownership member. Then the member type supplies correct lifetime and copy policy.
Deep copy does not mean copying every reachable object blindly. The class's value semantics determine which state is owned, shared or observed.
13. Rule of Three
If a class manually defines a destructor, copy constructor or copy assignment operator because it owns a raw resource, it usually needs all three:
- destructor releases the resource;
- copy constructor creates independent owned state;
- copy assignment safely replaces owned state.
Copy assignment can allocate new state before releasing old state to preserve the original object if allocation fails. Copy-and-swap is one technique, though direct member-based design is often simpler.
14. Move construction
Move construction initializes a new object by transferring resources from an expiring object:
Buffer(Buffer&& other) noexcept
: size_(std::exchange(other.size_, 0)),
data_(std::exchange(other.data_, nullptr)) {}
The source remains valid but no longer owns the transferred resource. Move operations commonly accept an rvalue reference.
Moving is valuable for resource-owning objects because transferring a handle is cheaper than duplicating the resource.
15. Move assignment
Move assignment replaces an existing object's state by transferring another object's resources:
Buffer& operator=(Buffer&& other) noexcept;
It must first release or replace the destination's current resource safely, handle self-move defensively when appropriate, take the source resource and leave the source valid.
Standard containers can prefer a noexcept move during reallocation. If a move might throw and copying is available, copying may preserve stronger guarantees.
16. std::move
std::move does not move anything by itself. It is a cast that treats its argument as an expiring value, enabling selection of move-aware overloads:
destination = std::move(source);
The selected move operation performs the transfer. Afterward, source is valid but its value may be unspecified. It may be destroyed, assigned a new value or used only through operations whose preconditions are satisfied.
Do not move from an object and then assume its earlier content remains.
17. Rule of Five
For a class manually managing a resource, the Rule of Five extends the Rule of Three with:
- move constructor;
- move assignment operator.
Declaring some special members affects implicit generation of others. Explicitly default or delete operations when the policy must be visible:
File(const File&) = delete;
File& operator=(const File&) = delete;
File(File&&) noexcept = default;
File& operator=(File&&) noexcept = default;
This describes a move-only owner.
18. Rule of Zero
The Rule of Zero recommends composing resource-managing members so the class needs no custom destructor, copy or move code:
struct Record {
std::string name;
std::vector<int> scores;
};
String and vector already implement resource ownership. Record receives correct generated copying, moving and destruction.
This is safer than manually implementing five interdependent operations and is a major reason to prefer standard containers and smart pointers.
19. Copy elision
Copy elision constructs a result directly in its final destination:
Record make_record() {
return Record{"Asha", {80, 90}};
}
Certain C++17 prvalue cases guarantee direct construction. Named return value optimization may eliminate movement from a named local when conditions allow.
Returning by value is therefore a normal efficient interface. Writing std::move on a local return can interfere with named return optimization and is usually unnecessary.
20. Dynamic initialization of objects
Objects can be initialized from runtime values:
std::string name;
std::cin >> name;
Student student{name};
Dynamic initialization here means construction uses runtime information, not necessarily dynamic memory. Automatic objects can be dynamically initialized.
When lifetime must be dynamic, use a smart pointer:
auto student = std::make_unique<Student>(name);
Ownership is then explicit and exception-safe.
21. Inheritance and destruction
Constructing a derived object initializes virtual bases, direct bases, members and then the derived constructor body. Destruction reverses this order.
A polymorphic base intended for deletion through a base pointer needs a virtual destructor:
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
Without it, deleting a derived object through Shape pointer is undefined behaviour.
22. Exception unwinding
When an exception propagates, destructors run for fully constructed automatic objects whose scopes are exited. This stack unwinding is the foundation of exception-safe RAII.
An object whose constructor never completed is not destroyed as a complete object, but its completed bases and members are destroyed. Resources acquired into members are therefore safer than resources acquired into raw fields and cleaned only in the destructor body.
23. Practical considerations
- Constructors establish valid initial state.
- Members initialize before the constructor body.
- Declaration order determines member initialization order.
- References and const members require initialization.
- Delegation calls another constructor of the same class.
- Automatic objects are destroyed in reverse construction order.
- RAII binds resource ownership to object lifetime.
- Copy construction creates a new object; copy assignment replaces existing state.
- Raw owning-pointer shallow copies can cause double deletion.
- The Rule of Five adds move construction and move assignment to the Rule of Three.
- The Rule of Zero prefers members that manage themselves.
- std::move is a cast enabling move-aware overload selection.
- Moved-from library objects remain valid but may have unspecified values.
- Copy elision can omit copy or move construction.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.