Every new call creates an obligation that has to be discharged with an associated delete at some later time. If the obligation is discharged by hand, every execution path of the program has to be correct. One forgetful statement, an early return, or an extra delete is sufficient to result in a leak, a double deletion, or a use after free error.
Consequently, modern C++ prefers designs in which an obligation is discharged automatically.
Consider this function: which allocates an integer, uses it, and should free it before returning, the release occurs at the end: allocation → use → release.
#include <iostream>
void process()
{
int* ptr { new int{ 42 } };
std::cout << *ptr << '\n';
// Release the dynamically allocated object
delete ptr;
}
Allocation, use, and release show up linearly, and the code appears correct. Yet any change that inserts an early exit prior to delete would render the function into a leak.
#include <iostream>
void process(bool earlyExit)
{
// Allocate this object dynamically
int* ptr { new int{ 42 } };
std::cout << *ptr << '\n';
if (earlyExit)
return; // Memory leaked
delete ptr;
}
When earlyExit becomes true, the function returns immediately without the statement delete being executed, leading to the allocated integer never being deallocated for the lifetime of the program. The compiler gives no warning; the error will manifest eventually, if enough leaks accumulate, when the program runs.
A second type of failure is caused by issuing two deletes on the same pointer.
#include <iostream>
int main()
{
int* ptr { new int{ 7 } };
delete ptr;
delete ptr; // Undefined behavior
return 0;
}
The first delete frees memory and returns it to the available pool; the second operates on a pointer that your program no longer owns, and as a consequence causes behavior that is undefined, be it immediate crashing, memory corruption, or simply failure occurring much later upon some other allocation.
And the third is that of dereferencing a pointer whose memory has already been deleted.
#include <iostream>
int main()
{
int* ptr { new int{ 99 } };
delete ptr;
// std::cout << *ptr << '\n'; // Undefined behavior
return 0;
}
When a memory address refers to memory previously pointed to by the commented line. In each case, the error could have resulted from minor, barely noticeable changes to the code.
This logic remains the same, but now is safe. Here the pointer is held by an object; the responsibility for freeing the memory and any other resource owned by the object, not shown for simplicity, resides within its destructor.
#include <iostream>
class IntOwner
{
public:
IntOwner(int value)
: ptr{ new int{ value } }
{
}
// Run this destructor when the object lifetime ends
~IntOwner()
{
delete ptr;
}
int get() const
{
return *ptr;
}
private:
int* ptr;
};
void process(bool earlyExit)
{
IntOwner owner{ 42 };
std::cout << owner.get() << '\n';
if (earlyExit)
return; // Destructor still runs
}
When the process function returns, either via normal falloff or an early return, the object owner goes out of scope and its destructor is invoked, in which we deallocate the integer allocated in the constructor. The delete is always invoked at the same point, and it is thus never forgotten. Nor can two delete statements be written for the same pointer that would access the previously freed block.
No member of owner can be accessed after the delete in its destructor has executed, and no programmer can dereference an invalid pointer through owner once it has been destroyed.
Exercise: Why Modern C++ Avoids Manual Memory ManagementStarter code, hints and solutionOpenClose
Practice: Why Modern C++ Avoids Manual Memory Management
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Replace the raw owning pointer with std::make_unique<int>(99). Print its value in a nested scope, remove the manual delete, and explain the automatic cleanup at scope exit.
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>
int main()
{
int* ptr { new int{ 99 } };
delete ptr;
// std::cout << *ptr << '\n'; // Undefined behavior
return 0;
}Hints
This short program relies on remembering delete ptr explicitly. Merely ending the pointer variable's scope would not release the int created by new.
For a modern version, use std::make_unique<int>(99) with <memory> and let its owner clean up automatically. Do not retain the manual delete when ownership moves to a smart pointer.
Worked solution
Manual new/delete is replaced by make_unique. The smart pointer is the sole owner and releases the int automatically at the closing brace, including on exception unwinding. There is no manual delete to forget or duplicate.
#include <iostream>
#include <memory>
int main()
{
{
auto owner = std::make_unique<int>(99);
std::cout << *owner << '\n';
}
std::cout << "Scope ended\n";
}
Example output
99
Scope ended
0 comments
No comments yet. Be the first.