The following code:
#include <new>
#include <iostream>
#include <cstdlib>
struct alignas(32) S
{
S() {throw 1;}
void* operator new(std::size_t count, std::align_val_t al)
{
return ::operator new(count, al);
}
void operator delete(void* ptr, std::align_val_t al)
{
std::cerr << "aligned delete\n";
::operator delete(ptr, al);
}
};
int main()
{
try
{
S* ps = new S;
}
catch(...)
{
}
}
will output aligned delete
.
Of course there is nothing wrong with that. However, after I added templated delete:
struct alignas(32) S
{
...
template <typename... Args>
void operator delete(void* ptr, Args... args)
{
std::cerr << "templated delete\n";
}
...
};
The output was the same as before.
If aligned delete is deleted, leaving only templated delete, then templated delete
will be output.
This behavior seems strange. According to [expr.new#28]:
A declaration of a placement deallocation function matches the declaration of a placement allocation function if it has the same number of parameters and, after parameter transformations ([dcl.fct]), all parameter types except the first are identical. If the lookup finds a single matching deallocation function, that function will be called; otherwise, no deallocation function will be called. If the lookup finds a usual deallocation function and that function, considered as a placement deallocation function, would have been selected as a match for the allocation function, the program is ill-formed. For a non-placement allocation function, the normal deallocation function lookup is used to find the matching deallocation function ([expr.delete]).
Strangely, I doesn't find the definition of "placement allocation function", maybe an allocation function with two or more parameters?
But it's almost certainly not "non-placement allocation function", otherwise it will look for a matching deallocation function according to [expr.delete], so it will never choose an unusual deallocation function. But when only templated delete is left, it is chosen.
In this case, the standard also mentions "If the lookup finds a single matching deallocation function, that function will be called; otherwise, no deallocation function will be called." But there are apparently two matching deallocation functions (templated and aligned). Will there be overload resolution?
And, "If the lookup finds a usual deallocation function and that function, considered as a placement deallocation function, would have been selected as a match for the allocation function, the program is ill-formed." aligned delete is a usual deallocation function, and it's chosen. So why does it actually work?
NOTE: I compiled with g++ 12.1.1 with three compile options -std=c++{17, 20, 23}
and got the same results.
EDIT: According to this answer, the above behavior occurs because the compiler behavior does not conform to the standard. So I ran some additional tests to get a clearer picture of the compiler's behavior.
When with single-parameter delete only:
void operator delete(void* ptr)
{
std::cerr << "single-parameter delete\n";
::operator delete(ptr);
}
it won't output. The weird thing is that the compiler won't issue any warnings.
This shows that the compiler actually considers aligned new to be a placement allocation function and violates the third sentence in the paragraph I quote because if it follows [expr.delete], single-parameter delete will be chosen.
When replacing aligned new with single-parameter new:
void* operator new(std::size_t count)
{
return ::operator new(count);
}
Output:
aligned delete only/with templated delete: aligned delete
templated delete only: none, with warning:
warning: no corresponding deallocation function for 'static void* S::operator new(std::size_t)'
Completely correct.
EDIT: I found that destroying delete isn't considered:
struct S
{
S() { throw 1; }
void operator delete(S* ptr, std::destroying_delete_t)
{
std::cerr << "destroying delete\n";
ptr->~S();
::operator delete(ptr);
}
void operator delete(void* ptr)
{
std::cerr << "single-parameter delete\n";
::operator delete(ptr);
}
};
will output single-parameter delete
.
But according to [expr.delete], destroying delete will be considered and has the highest priority, so it will be chosen.
However, destroying delete should definitely not be considered, because the object is not properly constructed at all.
Is that an issue, or am I missing something?