Important: This lesson uses classes in the code examples which we haven't covered yet, so you can SKIP it until you learn about classes, in the Classes and Objects category.
A manual release of a resource only occurs when all paths of the code remember to release the resource. Even one early return, lost error path, or simple omission is sufficient to leak memory or keep a resource in use. RAII removes this burden by binding the lifetime of a resource to the lifetime of a normal object.
The underlying idea is simple. One class acquires the resource in the constructor and releases it in the destructor. At the point of creation of an object of that class, the resource is acquired; when the object's lifetime comes to an end, the resource is released by the language itself. The programmer never needs to write the release manually.
A minimal example follows; the object owns a single heap allocated int.
#include <iostream>
class IntOwner
{
public:
IntOwner(int value)
: ptr{ new int{ value } }
{
}
IntOwner(const IntOwner&) = delete;
IntOwner& operator=(const IntOwner&) = delete;
~IntOwner()
{
delete ptr;
}
int get() const
{
return *ptr;
}
private:
int* ptr;
};
int main()
{
IntOwner owner{ 42 };
std::cout << owner.get() << '\n';
return 0;
}
When the object owner is created, the IntOwner class's constructor is run. The member initializer ptr{ new int{ value } } then allocates a heap int and stores its pointer in owner's ptr member. When the function main finally reaches its terminating brace, owner is destroyed and IntOwner's destructor is run.
The destructor runs delete ptr, releasing the memory on the heap.
In main, the programmer never wrote a direct delete; the compiler invoked the destructor upon leaving scope, effectively doing what had to be done.
In the following case with an early return from the scope:
#include <iostream>
class IntOwner
{
public:
IntOwner(int value)
: ptr{ new int{ value } }
{
}
IntOwner(const IntOwner&) = delete;
IntOwner& operator=(const IntOwner&) = delete;
~IntOwner()
{
delete ptr;
}
int get() const
{
return *ptr;
}
private:
int* ptr;
};
int main()
{
IntOwner owner{ 99 };
std::cout << owner.get() << '\n';
return 0; // Destructor still runs
}
The return statement immediately aborts the function main; however, the destructor for owner is guaranteed to be run prior to the function exiting. The memory has been freed. Had we used a raw pointer, the return statement would have skipped over any future delete statement, for example, in the body of main, that would have performed the cleanup, resulting in a leak. Since the release mechanism is bound to the object's destructor, we can never miss cleanup, regardless of how the scope is exited.
We can see how a bare pointer lacks such a guarantee:
#include <iostream>
int main()
{
int* ptr{ new int{ 7 } };
std::cout << *ptr << '\n';
// If an early return occurred here, the delete would be skipped
delete ptr;
return 0;
}
The delete has not yet been executed, and if we exit before reaching it by any means, the memory that we have allocated will not be reclaimed. In RAII, this cannot happen; the destructor is guaranteed to be called.
Also, the ownership is clear. owner truly owns its int. It remains an owner of that int for its entire lifetime, which coincides with the lifetime of the owner object. As soon as owner is destroyed and its destructor called, its ownership and access to the memory has ended and the memory is released. No stray pointer can be lurking in the environment to "own" the same memory.
Exercise: Introduction to RAIIStarter code, hints and solutionOpenClose
Practice: Introduction to RAII
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Place IntOwner in a nested scope, report when its destructor releases the int, and print a message after the scope. Disable copying of this simple owning class.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
class IntOwner
{
public:
IntOwner(int value)
: ptr{ new int{ value } }
{
}
IntOwner(const IntOwner&) = delete;
IntOwner& operator=(const IntOwner&) = delete;
~IntOwner()
{
delete ptr;
}
int get() const
{
return *ptr;
}
private:
int* ptr;
};
int main()
{
IntOwner owner{ 42 };
std::cout << owner.get() << '\n';
return 0;
}Hints
IntOwner acquires its int in the constructor and deletes it in the destructor. Find the scope exit that triggers cleanup for owner automatically.
Use a nested pair of braces to trigger destruction before a later output statement. Delete the copy constructor and copy assignment to prevent sharing this owning raw pointer.
Worked solution
The owner is placed in an inner block and its destructor reports release. On leaving that block the destructor deletes the int before the After block message. Copying is explicitly disabled so this simple RAII owner cannot accidentally share its raw pointer.
#include <iostream>
class IntOwner
{
int* ptr;
public:
explicit IntOwner(int value) : ptr{new int{value}} {}
IntOwner(const IntOwner&) = delete;
IntOwner& operator=(const IntOwner&) = delete;
~IntOwner() { delete ptr; std::cout << "Int released\n"; }
int get() const { return *ptr; }
};
int main()
{
{
IntOwner owner{42};
std::cout << owner.get() << '\n';
}
std::cout << "After block\n";
}
Example output
42
Int released
After block
0 comments
No comments yet. Be the first.