If I write operators new and delete for a class, do I have to write all of their overloads?
Asked Answered
B

3

11

The C++ Reference page lists 8 class specific overloads for global new operators. Four of those were added for 2017 version of C++.

Class-specific allocation functions

void* T::operator new  ( std::size_t count );   
void* T::operator new[]( std::size_t count );
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)

Class-specific placement allocation functions

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)

That website also lists 10 class specific versions of global delete operators, of which 4 were introduced for 2017.

Class-specific usual deallocation functions

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)
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)

Class-specific placement deallocation functions

void T::operator delete  ( void* ptr, args... );
void T::operator delete[]( void* ptr, args... );

If I write a C++ class with new and delete operators, do I need to overload all of those? I am ignoring the replaceable global operators since I am only writing class specific operators.

This other question provides info on writing ISO compliant new and delete operators, but does not say if I should overload all of them, or just some.

The answer to this question about class specific new and delete operators does not say whether to replace all or just some of them.

If you can provide citations from the C++ Standard or comments by C++ memory experts, that would help.

Breccia answered 6/8, 2017 at 20:38 Comment(4)
Seems to me like this answer has the information you're looking for.Reptile
Well, if you want to use a custom allocation scheme, I think it's prudent to overload all the operators that do real allocation and deallocation. That, or explicitly delete the ones you do not want to support.Ataliah
You could use a template to define placement new and delete. This would allow the compiler to generate the type specialized code.Membership
@CodyGray Thanks, Cody, but that does not answer my question. It says why to write the new operators, not which to overload. It says to overload class specific ones to improve performance or reduce fragmentation. It does not say anything about whether if writing some is sufficient, or if all are necessary.Breccia
M
2

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;
}
Merylmes answered 7/8, 2017 at 17:34 Comment(1)
Thanks. Best answer so far. Very detailed and understandable.Breccia
S
2

You only need to overload the versions of new and delete that you use. According to the example in [class.free], defining an operator new function in a class will hide all the global operator new functions. This is the same as defining a method with the same name as a base class function or global function hides the base or global versions.

Note that operator new and operator new[] are different names, so overloading operator new by itself will not hide the global operator new[] functions.

Subtropics answered 6/8, 2017 at 21:5 Comment(0)
R
2

If I write a C++ class with new and delete operators, do I need to overload all of those?

No you don't need to overload all of them. At a minimum, you need to overload the operators you need to customize.

I think we can assume you are doing something specific in the overloaded operators, else you wouldn't have needed them anyway.

The question becomes more should I overload all of these?

Yes, you probably should. It would be surprising if the the code did completely different things depending on the form of the new or delete used in the code, e.g.

auto* obj1 = new Obj{};
// vs
auto* obj2 = new Obj[5];

If the new operator has some special initialization applied, it would reasonably be expected that both forms would do that initialization.

A flip side to this is that if the other forms are not applicable, then favor deleting (= delete) those overloads entirely.

The C++ operators come in "sets", arithmetic, stream insertions and extractions, relational etc. It is common practice that when one of the operators in a set is overloaded, the others are as well.

It doesn't always apply, but generally does. E.g. concatenation operations often have the operator+ and operator+=, but not the operator- and operator-=

Rascon answered 7/8, 2017 at 8:11 Comment(1)
Favor deleting them entirely besides making them private.Ataliah
M
2

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;
}
Merylmes answered 7/8, 2017 at 17:34 Comment(1)
Thanks. Best answer so far. Very detailed and understandable.Breccia

© 2022 - 2024 — McMap. All rights reserved.