5

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?

  • 1
    The "aligned delete" function will be an exact match, while the templated delete will not. – Some programmer dude Aug 13 '22 at 14:33
  • 1
    This seems to be just a case of [overload resolution](https://en.cppreference.com/w/cpp/language/overload_resolution). The aligned operator is an exact match, so it is preferred over the template. – nickie Aug 13 '22 at 14:35
  • @Someprogrammerdude Nothing in the quote in the question is saying that overload resolution is performed. It specifically talks about _lookup_ and states that multiple results (which can't happen after overload resolution) will result in no deallocation function being called. – user17732522 Aug 13 '22 at 22:17
  • @nickie See above. – user17732522 Aug 13 '22 at 22:17

1 Answers1

1

Strangely, I doesn't find the definition of "placement allocation function", maybe an allocation function with two or more parameters?

The definition seems to be missing. This is considered in the currently open CWG issue 2592. The resolution suggested in the issue indicates that the intent is that a placement allocation function is any allocation function which is not a usual allocation function, defined as an allocation function "with no parameters after the first or with a single parameter of type std::align_val_t after the first." and analogously that a placement deallocation function is any deallocation function which is not a usual deallocation function.

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 a unusual deallocation function. But when only templated delete is left, it is chosen.

By the clarified definitions above, both the allocation function you are showing and the non-templated deallocation function are usual. Therefore "For a non-placement allocation function, the normal deallocation function lookup is used to find the matching deallocation function ([expr.delete])." applies and will find the usual deallocation function.


Assuming that the allocation function and the non-templated deallocation function were indeed placement variants:

But there are apparently two matching deallocation functions (templated and aligned). Will there be overload resolution?

There is only one matching deallocation function declaration. Note that the first sentence in the paragraph you quote talks about matching declarations of functions. There is no mention of overload resolution in any form. There is also no mention of function templates or template argument deduction in any form. (A function template is not a function and a function is not a function template.)

According to the sentence only (non-templated) functions can be matching deallcocation functions and the decision for a match is done by simply comparing the types in the function parameter list to those in the declaration of the allocation function, which also must be a declared function, not a function template, in order for the sentence to apply.

Consequently your deallocation function template isn't a match and therefore the "If the lookup finds a single matching deallocation function" path is chosen.

See the open CWG issue 1628 which discusses the problem that allocation and deallocation function templates can never match and how this might be improved.


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 a unusual deallocation function.

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?

If there was a mismatch between the allocation and the non-templated deallocation function being placement variants, then the program should be ill-formed per either of your two reasonings here.


If aligned delete is deleted, leaving only templated delete, then templated delete will be output.

But when only templated delete is left, it is chosen.

I might be missing something, but as far as I can tell that is incorrect behavior. If lookup doesn't find anything usual in the non-placement case or the chosen candidate is not usable in the placement case, then the program should be ill-formed. If lookup doesn't find anything matching in the placement case no deallocation function should be called. I don't see any exceptions.

user17732522
  • 53,019
  • 2
  • 56
  • 105