No, you don't need to write all variations of the new and delete operators for your class.
There are multiple reasons to prefer some versions of new and delete over others. I will describe each reason separately.
Almost always prefer the delete operators that have a size parameter over those without one.
When I write delete operators for a base class that provides memory handling for other classes, I use these versions of the delete operators
void T::operator delete ( void* ptr, std::size_t sz );
void T::operator delete[]( void* ptr, std::size_t sz );
void T::operator delete ( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)
and deliberately omit or =delete
these versions.
void T::operator delete ( void* ptr );
void T::operator delete[]( void* ptr );
void T::operator delete ( void* ptr, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::align_val_t al ); // (since C++17)
The reason is that the std::size_t sz
parameter tells me the size of the object or size of the array. I can't know the sizes of the derived class' objects when I write my base class, so using the size parameter helps. Some of my memory handlers segregate the objects by size (easier to pool memory when all the chunks are the same size). I can use the size parameter to quickly choose which memory pool to search, rather than searching all of them. That turns a O(n) algorithm into a O(1) action.
Some of my memory allocators use a "chain model" instead of a "block model", and the size parameter helps for deleting there too. (I call a memory allocator a "block model" if it preallocates a huge chunk and then partitions the chunk into separate blocks like an array. I call a memory handler a "chain model" if each chunk points to previous and next chunks like a linked list or chain.) So when somebody deletes a chunk from a chain of memory chunks, I want the delete operator to know the chunk being deleted is the correct size. I can put an assertion in the delete operation that asserts (size == address of next chunk - address of this chunk).
Where appropriate, prefer the new and delete operators with the alignment parameter.
Now that C++17 provides an alignment parameter for new operators, use them if you need them. If you need performance, align your objects on 4, 8, or 16 byte boundaries, do so! It makes the program a little faster.
So let's say you have an alignment-aware memory allocator. It knows that some objects are best stored on 4 byte boundaries because those objects are small and you can squeeze more into memory if you use 4 byte boundaries. It also knows some objects are best aligned on 8 byte boundaries because those objects are used often.
Your memory handler will know this if it provides the correct new operators and derived classes provide the correct values for alignments.
The 2017 C++ Standard says:
When allocating objects and arrays of objects whose alignment exceeds STDCPP_DEFAULT_NEW_ALIGNMENT, overload resolution is performed twice: first, for alignment-aware function signatures, then for alignment-unaware function signatures. This means that if a class with extended alignment has an alignment-unaware class-specific allocation function, it is the function that will be called, not the global alignment-aware allocation function. This is intentional: the class member is expected to know best how to handle that class.
This means the compiler will check for new and delete operators with the alignment parameter, and then check for operators without the alignment parameter.
If you have an alignment-aware memory handler, then always provide these new operators, even if you also want to give your client code the option of ignoring alignment.
void* T::operator new ( std::size_t count, std::align_val_t al ); // (since C++17)
void* T::operator new[]( std::size_t count, std::align_val_t al ); // (since C++17)
void* T::operator new ( std::size_t count,
std::align_val_t al, user-defined-args... ); // (since C++17)
void* T::operator new[]( std::size_t count,
std::align_val_t al, user-defined-args... ); // (since C++17)
You can force code to provide the alignment parameter if you provide the above new operators and omit or =delete
these overloads.
void* T::operator new ( std::size_t count );
void* T::operator new[]( std::size_t count );
void* T::operator new ( std::size_t count, user-defined-args... );
void* T::operator new[]( std::size_t count, user-defined-args... );
Use class specific placement-new operators to provide hints.
Let's say you wrote a class that allocates several data members, and you want all those data members to be located on the same memory page. If the data is spread across several memory pages, the CPU will have to load different memory pages into the L1 or L2 cache just so you can access the member data for an object. If your memory handler can place all of an object's data members onto the same page, then your program will run faster because the CPU will not need to load multiple pages into cache.
These are the class specific placement new operators.
void* T::operator new ( std::size_t count, user-defined-args... );
void* T::operator new[]( std::size_t count, user-defined-args... );
void* T::operator new ( std::size_t count,
std::align_val_t al, user-defined-args... ); // (since C++17)
void* T::operator new[]( std::size_t count,
std::align_val_t al, user-defined-args... ); // (since C++17)
Overload them to look like this by providing a hint parameter.
void* T::operator new ( std::size_t count, void* hint );
void* T::operator new[]( std::size_t count, void* hint );
void* T::operator new ( std::size_t count, std::align_val_t al, void* hint ); // (since C++17)
void* T::operator new[]( std::size_t count, std::align_val_t al, void* hint ); // (since C++17)
The hint parameter tells the memory handler to try to place the object not at the location of that hint address, but on the same page as the hint address.
Now you can write a class that looks like this which is derived from your memory handling class.
class Foo : public MemoryHandler
{
public:
Foo();
...
private:
Blah * b_;
Wham * f_;
};
Foo::Foo() : b_( nullptr ), f_( nullptr )
{
// This should put data members on the same memory page as this Foo object.
b_ = new ( this ) Blah;
f_ = new ( this ) Wham;
}
template
to define placementnew
anddelete
. This would allow the compiler to generate the type specialized code. – Membership