Mastering Memory Management in C++
Efficient Object Construction and Destruction
C++17 introduced a set of utility functions in <memory>
that revolutionize the way we construct and destroy objects without allocating or deallocating memory. These functions, prefixed with std::uninitialized_
, enable us to create, copy, and move objects to an uninitialized memory area with ease. Moreover, std::destroy_at()
allows us to destruct an object at a specific memory address without deallocating the memory.
A New Era of Memory Management
Let’s revisit the previous example, rewritten using these new functions:
cpp
auto* memory = std::malloc(sizeof(User));
auto* user_ptr = reinterpret_cast<User*>(memory);
std::uninitialized_fill_n(user_ptr, 1, User{"john"});
std::destroy_at(user_ptr);
std::free(memory);
In C++20, std::construct_at()
takes it a step further, replacing the std::uninitialized_fill_n()
call:
cpp
std::construct_at(user_ptr, User{"john"}); // C++20
The Importance of Low-Level Memory Facilities
While these low-level memory facilities provide unparalleled control, it’s essential to use them judiciously. In a C++ code base, reinterpret_cast
and memory utilities should be kept to an absolute minimum.
The new and delete Operators: Unveiled
When we use the new
and delete
expressions, the function operator new
is responsible for allocating memory. This operator can be either a globally defined function or a static member function of a class. Overloading the global operators new
and delete
can be useful when analyzing memory usage.
Customizing Memory Allocation
We can overload the new
and delete
operators to gain fine-grained control over memory allocation. Here’s an example:
“`cpp
auto operator new(size_t size) -> void* {
void* p = std::malloc(size);
std::cout << “allocated ” << size << ” byte(s)\n”;
return p;
}
auto operator delete(void* p) noexcept -> void {
std::cout << “deleted memory\n”;
return std::free(p);
}
“`
Verifying that our overloaded operators are being used is straightforward:
cpp
auto* p = new char{'a'}; // Outputs "allocated 1 byte(s)"
delete p; // Outputs "deleted memory"
Array Allocation and Deallocation
When creating and deleting arrays of objects using new[]
and delete[]
expressions, we can overload the operator new[]
and operator delete[]
operators:
“`cpp
auto operator new -> void* {
void* p = std::malloc(size);
std::cout << “allocated ” << size << ” byte(s) with new[]\n”;
return p;
}
auto operator delete noexcept -> void {
std::cout << “deleted memory with delete[]\n”;
return std::free(p);
}
“`
Remember, when overloading operator new
, it’s essential to also overload operator delete
. Functions for allocating and deallocating memory come in pairs, ensuring that memory is deallocated by the allocator that allocated it.