| Document #: | P2719R6 [Latest] [Status] |
| Date: | 2026-06-08 |
| Project: | Programming Language C++ |
| Audience: |
EWG and CWG |
| Reply-to: |
Louis Dionne <ldionne@apple.com> Oliver Hunt <oliver@apple.com> Vlad Serebrennikov <serebrennikov.vladislav@gmail.com> |
C++ currently provides
two ways of customizing the creation of objects in new expressions.
First, operator new
can be provided as a static member function of a class, like void* T::operator new.
If such a declaration is provided, an expression like new T(...)
will use that allocation function. Otherwise, the global version of
operator new
can be replaced by users in a type-agnostic way, by implementing void* operator new(size_t)
and its variants. A similar mechanism exists for
delete-expressions.
This paper proposes an extension to new-expressions and
delete-expressions to provide the concrete type being
[de]allocated to the allocation functions. This is achieved via the use
of an additional std::type_identity<T>
tag argument that allows the provision of the concrete type to operator new
and operator delete.
In addition to providing valuable information to the allocator, this
allows the creation of type-specific operator new
and operator delete
for types that cannot have intrusive class-scoped operators
specified.
At a high level, this allows defining allocation and deallocation functions like:
void* operator new(std::type_identity<mylib::Foo>, std::size_t n, std::align_val_t align) {
...
}
void operator delete(std::type_identity<mylib::Foo>, void* ptr, std::size_t n, std::align_val_t align) {
...
}However, it also allows providing these functions for a family of types, which is where this feature becomes interesting:
template <class T>
requires use_special_allocation_scheme<T>
void* operator new(std::type_identity<T>, std::size_t n, std::align_val_t align) { ... }
template <class T>
requires use_special_allocation_scheme<T>
void operator delete(std::type_identity<T>, void* ptr, std::size_t n, std::align_val_t align) { ... }std::type_identity
parameter for in-class T::operator new
for consistencystd::type_identity
as the first parameter[expr.new]
and [expr.delete]operator new
in constant expressionsstd::align_val_tstd::type_identity<U>
rather than std::type_identity<T>
as that has caused repeated confusion with std::type_identity_t<>
during discussionsKnowledge of the type being [de]allocated in a
new-expression is necessary in order to achieve certain levels
of flexibility when defining a custom allocation function. However, even
when defining T::operator new
in-class, the only information available to the implementation is the
type declaring the operator, not the type being allocated. This results
in developers various creative (often macro-based) mechanisms to define
these allocation functions manually, or circumventing the
language-provided allocation mechanisms entirely in order to track the
allocated types.
However, in addition to these intrusive mechanisms being cumbersome and error-prone, they do not make it possible to customize how allocation is performed for types controlled by a third-party, or to customize allocation for an open set of types.
Beyond these issues, a common problem in we see in the wild is
codebases overriding the global (and untyped) operator new
via the usual link-time mechanism and running into problems because they
really only intended for their custom operator new
to be used within their own code, not by all the code in their process.
For example, we’ve seen scenarios where multiple libraries attempt to
replace the global operator new
and end up with a complex ODR violation bug that depends on how the
dynamic linker resolved weak definitions at load time – not very user
friendly. By providing the concrete type information to allocators at
compile time, it becomes possible for authors to override operator new
for a family of types that they control without overriding it for the
whole process, which is what they actually want.
A few years ago, Apple published a blog post explaining a technique used inside its kernel (XNU) to mitigate various exploits. At its core, the technique roughly consists in allocating objects of each type in a different bucket. By collocating all objects of the same type into the same region of memory, it becomes much harder for an attacker to exploit a type confusion vulnerability. Since its introduction in the kernel, this technique alone has been by far the most effective at mitigating type confusion vulnerabilities.
In a world where security is increasingly important, it may make sense for some code bases to adopt mitigation techniques such as this one. However, these techniques require a large-scale and almost system-wide customization of how allocation is performed while retaining type information, which is not supported by C++ today. While not sufficient in itself to make C++ safer, the change proposed in this paper is a necessary building block for technology such as the above which can greatly improve the security of C++ applications.
Today, the compiler performs a
lookup in the allocated type’s class scope (for T::operator new),
and then a lookup in the global scope (for ::operator new)
if the previous one failed. Once the name lookup has been done and the
compiler has decided whether it was looking for T::operator new
or ::operator new,
name lookup will not be done again even if the steps that follow were to
fail. From here on, let’s denote by
NEW the set of candidates found by
the name lookup process.
The compiler then performs overload
resolution on that set of candidates using the language-specified
optional implicit parameters, and if present any developer-provided
placement arguments. It does so by assembling an argument list that
depends on whether T has a
new-extended alignment or not. For the sake of simplicity, assume that
T does not have a new-extended
alignment. The compiler starts by performing overload resolution as-if
the following expression were used:
NEW(sizeof(T), args...)If that succeeds, the compiler selects the overload that won. If it does not, the compiler performs overload resolution again as-if the following expression were used:
NEW(sizeof(T), std::align_val_t(alignof(T)), args...)If that succeeds, the compiler selects the overload that won. If it
does not, the program is ill-formed. For a type
T that has new-extended alignment,
the order of the two overload resolutions performed above is simply
reversed.
Delete-expressions behave similarly, with lookup being performed in
the context of the static type of the expression. The overload
resolution process then works by preferring a destroying delete,
followed by an aligned delete (if the type has new-extended alignment),
followed by the usual operator delete
(with or without a
size_t
parameter depending on whether the considered operator delete
is a member function or not).
This proposal adds a new implicit tag argument of type std::type_identity<T>
to operator new
and operator delete
that is incorporated into the existing overload resolution logic with a
higher priority than existing implicit parameters. To avoid conficts
with existing code, this parameter is placed as the first argument to
the operator, preceding the size or subject pointer. To avoid the
complexities of ADL, this proposal does not change any of the name
lookup rules associated to new and delete
expressions: it only changes the overload resolution that happens once a
name has been found.
For the declaration of a type-aware [de]allocation operator to be
valid, we explicitly require that the parameter be a (potentially
dependent) specialization of std::type_identity,
but not a fully dependent type. In other words, the compiler must be
able to tell that the first parameter is of the form std::type_identity<T>
at the time of parsing the declaration, but before the declaration has
been instantiated in the case of a template. This is analogous to the
current behavior where we require specific concrete types in the
parameter list even in dependent contexts.
Once a set of candidate declarations has been found we perform the
same prioritized overload resolution steps, only with the addition of
std::type_identity<T>,
with a higher priority than the existing size and alignment parameters.
For illustration, here is how overload resolution changes
(NEW is the set of candidates found
by name lookup for operator new,
and DELETE is the equivalent for
operator delete).
If the user writes new T(...),
the compiler checks (in order):
Before
|
After
|
|---|---|
|
|
|
|
If the user writes
delete ptr,
the compiler checks (in order):
Before
|
After
|
|---|---|
|
|
|
|
If multiple candidates match a given set of parameters, candidate prioritisation and selection is performed according to usual rules for overload resolution.
When a constructor throws an exception, a call to operator delete
is made to clean up. Overload resolution for this call remains
essentially the same, the only difference being that the selected operator delete
must have the same type-awareness as the preceding operator new
or the program is considered ill-formed.
For clarity, in types with virtual destructors, operator delete
is resolved using the destructor’s class as the type being deallocated
(this matches the existing semantics of being equivalent to performing
delete this
in the context of the class’s non virtual destructor).
struct SingleClass { };
struct UnrelatedClass { };
struct BaseClass { };
struct SubClass1 : BaseClass { };
struct SubClass2 : BaseClass { };
struct SubClass3 : BaseClass { };
void* operator new(std::type_identity<SingleClass>, std::size_t, std::align_val_t); // (1)
template <typename T> void* operator new(std::type_identity<T>, std::size_t, std::align_val_t); // (2)
template <std::derived_from<BaseClass> T>
void* operator new(std::type_identity<T>, std::size_t, std::align_val_t); // (3)
void* operator new(std::type_identity<SubClass2>, std::size_t, std::align_val_t); // (4)
void* operator new(std::type_identity<SubClass3>, std::size_t, std::align_val_t) = delete; // (5)
struct SubClass4 : BaseClass {
void *operator new(size_t); // (6)
};
void f() {
new SingleClass(); // calls (1)
new UnrelatedClass(); // calls (2)
new BaseClass(); // calls (3) with T=BaseClass
new SubClass1(); // calls (3) with T=SubClass1
new SubClass2(); // calls (4)
new SubClass3(); // resolves (5) reports error due to deleted operator
new SubClass4(); // calls (6) as the class scoped operator wins
new int(); // calls (2) with T=int
}[Note:
The above is for illustrative purposes only: it is a bad idea to
provide a fully unconstrained type-aware operator new. — end note ]
// In-class operator
class SubClass1;
struct BaseClass {
template <typename T>
void* operator new(std::type_identity<T>, std::size_t, std::align_val_t); // (1)
void* operator new(std::type_identity<SubClass1>, std::size_t, std::align_val_t); // (2)
};
struct SubClass1 : BaseClass { };
struct SubClass2 : BaseClass { };
struct SubClass3 : BaseClass {
void *operator new(std::size_t); // (3)
};
struct SubClass4 : BaseClass {
template <typename T>
void *operator new(std::type_identity<T>, std::size_t, std::align_val_t); // (4)
};
void f() {
new BaseClass; // calls (1) with T=BaseClass
new SubClass1(); // calls (2)
new SubClass2(); // calls (1) with T=SubClass2
new SubClass3(); // calls (3)
new SubClass4(); // calls (4) with T=SubClass4
::new BaseClass(); // ignores in-class operators and uses appropriate global operator
}There are many cases where projects may not want types to be
allocated and deallocated via
new and
delete
operators. Doing so today requires injecting operators into the relevant
types, which often results in extensive use of macros. This proposal
allows constraint based selection of target types, and as such can be
leveraged to specify deleted operators, and so automatically prevent
their use e.g.
template <typename T> concept SelectionConstraint = ...;
template <SelectionConstraint T> void *operator new(std::type_identity<T>, std::size_t, std::align_val_t) = delete;
[. . .]
template <SelectionConstraint T> void operator delete(std::type_identity<T>, void *, std::size_t, std::align_val_t) = delete;
[. . .]The template arguments to a type aware operator new or delete are not
required to be directly applied to std::type_identity,
but are simply available for usual template deduction, so a type aware
allocation function can be defined to operate over a template type,
e.g.
template <typename T, int N>
struct MyArrayType {
// …
};
template <typename T, int N>
void *operator new(std::type_identity<MyArrayType<T, N>>, size_t, std::align_val_t, ...) {
// …
}
// …
// calls the above operator new<int, 5>(std::type_identity<MyArrayType<int, 5>>, ...)
auto A = new MyArrayType<int, 5>;Like basic type-aware allocation and deallocation functions,
type-aware functions can only be declared in a class scope or the global
scope. One additional constraint that is placed on type-aware
declarations is that any scope that contains a type-aware operator new
or operator new[]
must also contain a corresponding operator delete
or operator delete[],
and vice versa. It is not required that both be type-aware, but if
either is, then both allocation and deallocation functions must belong
to the same scope. This reduces the chance of unintentional use of
operator
delete and
new from
mismatching allocation interfaces.
Declarations of type-aware allocation functions require the
declarations of both operator
new and
delete to
include all existing optional allocation function parameters, e.g. a
type-aware operator new
or new[]
must have at least three parameters: std::type_identity<U>,
std::size_t,
and std::align_val_t.
Similarly, an operator delete
or delete[]
has at least four parameters: std::type_identity<U>,
void *,
std::size_t,
and std::align_val_t.
Placement parameters are permitted for type-aware allocation
functions, and behave identically to those of basic allocation functions
with two contained behavioral changes. The first is concerned with how
the allocation function is selected for a given new-expression. If the first placement
argument is of type std::align_val_t,
then the implicit alignment argument is suppressed.
The second change removes the leak hazard present in the existing allocation logic. Currently, when a matching deallocation function is selected to release the allocated memory in a case constructor throws an exception, it is not an error to find more than one deallocation function or to find none. In such case no deallocation function is selected, and the memory is leaked. Now, if the object was allocated with a type-aware allocation function, the program is ill-formed if a matching deallocation function cannot be selected.
In the current specification it is not possible for an author to
specify a constexpr operator new
or operator delete.
Instead, using a new-expression or a delete-expression
in a
constexpr
context requires that the resolved operator be ::operator new
/ ::operator delete,
and the compiler elides the call entirely. In particular, if an in-class
T::operator new
is defined and the new-expression resolves to it, the
new-expression is not
constexpr-friendly.
That seems to be an arbitrary limitation.
We clearly don’t want or need such a limitation for typed operator new.
Our solution to that is to treat a global typed operator new
(without placement parameters) just like an untyped ::operator new,
i.e. as a replaceable global allocation function. While that
does mean the compiler will assume the behaviour of typed operator new,
that seems like a reasonable assumption inside
constexpr.
We apply the same rationale to operator new[],
operator delete,
and operator delete[]
in order to fully support type aware allocators where existing global
allocators are supported.
There is a question as to whether the type-identity should include qualifiers. In previous drafts this was not addressed, and retaining the qualifiers was implied. After consideration we have realised that this would be surprising, is inconsistent with how the type being allocated is presented to the developer throughout the process of allocation and deallocation, and results in the type-identity trivially diverging between such allocation and deallocation e.g.
const T *t = new T;
...
delete t;There is currently no language mechanism to enforce that operator new
and operator delete
are defined as a pair. That is a potential source of confusion and bugs.
However, since we are introducing a new form of operator new
and operator delete,
we are free to change these rules, and it would be simple to do so. In
[expr.new] we suggest
adding the follow paragraph to force the selected operator delete
to be in the same scope as its matching operator new.
We believe that the vast majority of use cases will already assume
that’s the case, and this would only catch a potentially common
misuse.
The proposal currently continues to consider optional implicit parameters to be optional. During edge case testing we found that the non-universality of sized deallocation creates a hazard where a less constrained sized deallocation may be selected over a more constrained unsized deallocation function. We believe it is actually reasonable to update the language to make all currently optional implicit parameters mandatory for type aware allocators.
We believe this is a reasonable step to take, as in the future any
type-oriented implicit parameters can be based on the std::type_identity
parameter, which would reduce the exponential increase in new allocation
and deallocation APIs.
In addition to resolving the hazard derived from compiler mode flag changes this also simplifies lookup semantics for the operators, and resolves the problem of sized deallocation for exception cleanup in the context of type aware allocation and deallocation (P3492 addresses this problem for environments where type aware allocation may not be an option).
The following wording changes are necessary:
In [basic.stc.dynamic.allocation] paragraph 1 and [basic.stc.dynamic.deallocation] paragraph 3
If a type-identity parameter is provided, the otherwise optional parameters are required to be present and specified with non-dependent types.
[expr.new] paragraph 29 is modified:
29 A declaration of a type aware placement deallocation function matches the declaration of a type aware placement deallocation function if the types of the type-identity parameter, and each parameter after the mandatory parameters are the same. Otherwise, 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]).
…
30 If a new-expression calls a deallocation function, it passes the value returned from the allocation function call as the argument of type
void*as the address parameter. If the deallocation operator has a type-identity parameter, the allocation size is passed as the third parameter of type size_t.…
std::type_identity<T>
vs “raw” template argumentIn an earlier draft, this paper was proposing the following
(seemingly simpler) mechanism. Instead of using std::type_identity<T>
as a tag, the compiler would search as per the following expression:
operator new<T>(sizeof(T), args...)The only difference here is that we’re passing the type being
allocated directly as a template argument instead of using a std::type_identity<T>
tag parameter. Unfortunately, this has a number of problems, the most
significant being that it’s not possible to distinguish the
newly-introduced type-aware operator from existing template operator new
and operator delete
declarations.
For example, a valid operator new
declaration today would be:
template <class ...Args>
void* operator new(std::size_t, Args...);Hence, an expression like new (42) int(3)
which would result in a call like operator new<int>(sizeof(int), 42)
could result in this operator being called with a meaning that isn’t
clear – is it a type-aware (placement) operator or a type-unaware
placement operator? This also means that existing and legal operators
could start being called in code bases that don’t expect it, which is
problematic.
Beyond that being confusing for users, this also creates a legitimate problem for the compiler since the resolution of new and delete expressions is based on checking various forms of the operators using different priorities. In order for this to make sense, the compiler has to be able to know exactly what “category” of operator a declaration falls in, so it can perform overload resolution at each priority on the right candidates.
Finally, when a constructor in a new-expression throws an
exception, an operator delete
that must be a usual deallocation function gets called to clean
up. If there is no matching usual deallocation function, no cleanup is
performed. Using a template parameter instead of a tag argument could
lead to code where no cleanup happened to now find a valid usual
deallocation function and perform a cleanup.
Taken together we believe these issues warrant the use of an explicit tag parameter.
std::type_identity<T>
vs T*This proposal uses std::type_identity<T>
as a tag argument rather than passing a first argument of type
T*. At first
sight, passing
T* as a
first tag argument seems to simplify the proposal and decouple the
compiler from the standard library.
However, this approach hides an array of subtle problems that are
avoided through the use of std::type_identity.
The first problem is the value being passed as the tag parameter. Given operator signatures of the form
template <class T> void *operator new(T*, size_t);
template <class T> void operator delete(T*, void*);Under the hood, the compiler could perform calls like
// T* ptr = new T(…)
operator new<T>((T*)nullptr, sizeof(T));
// delete ptr
operator delete<T>((T*)nullptr, ptr);A developer could reasonably be assumed to know that the tag
parameter to operator new
can’t be anything but a null pointer. However, for operator delete
we can be assured that people will be confused about receiving two
pointer parameters, where the explicitly typed parameter is
nullptr.
Also note that we cannot pass the object pointer through that parameter
as operator delete
is called after the object has been destroyed. Passing the memory to be
deallocated through a typed pointer is an incitation to use that memory
as a T object, which would be
undefined behavior.
A scenario we have discussed is developers wishing to provide custom [de]allocation operators for a whole class hierarchy. When using a typed pointer as the tag, this would be written as:
struct Base { };
void* operator new(Base*, std::size_t);
void operator delete(Base*, void*);This operator would then also match any derived types of
Base, which may or may not be
intended. If not intended, the conversion from
Derived* to
Base* would
be entirely silent and may not be noticed. Furthermore, this would
basically defeat the purpose of providing type knowledge to the
allocator, since only the type of the base class would be known. We
believe that the correct way of implementing an operator for a hierarchy
is this:
struct Base {
template <class T>
void* operator new(std::type_identity<T>, std::size_t); // T is the actual type being allocated
template <class T>
void operator delete(std::type_identity<T>, void*);
};Or alternatively, in the global namespace:
template <std::derived_from<Base> T>
void* operator new(std::type_identity<T>, std::size_t);
template <std::derived_from<Base> T>
void operator delete(std::type_identity<T>, void*);For these reasons, we believe that a tag type like std::type_identity
is the right design choice.
std::type_identity<T>When writing this paper, we went back and forth of the order of arguments. This version of the paper proposes:
operator new(std::type_identity<T>, std::size_t, placement-args...)
operator new(std::type_identity<T>, std::size_t, std::align_val_t, placement-args...)
operator delete(std::type_identity<T>, void*)
operator delete(std::type_identity<T>, void*, std::size_t)
operator delete(std::type_identity<T>, void*, std::size_t, std::align_val_t)Another approach would be:
operator new(std::size_t, std::type_identity<T>, placement-args...)
operator new(std::size_t, std::align_val_t, std::type_identity<T>, placement-args...)
operator delete(void*, std::type_identity<T>)
operator delete(void*, std::size_t, std::type_identity<T>)
operator delete(void*, std::size_t, std::align_val_t, std::type_identity<T>)The existing specification allows for the existence of template (including variadic template) declarations of operator new and delete, and this functionality is used in existing code bases. This leads to problems compiling real world code where overload resolution will allow selection of a non-SFINAE-safe declaration and subsequently break during compilation.
Placing the tag argument first ensures that no existing operator definition can match, and so we are guaranteed to be free from conflicts.
operator delete
is a usual deallocation functionAllowing type-aware operator delete
does require changes to the definition of usual deallocation functions,
but the changes are conceptually simple and the cost of not supporting
this case is extremely high.
In the current specification, we place very tight requirements on
what an operator delete
declaration can look like in order to be considered a usual
deallocation function. The reason this definition previously
disallowed function templates is that all of the implicit parameters are
monomorphic types. That restriction made sense previously.
However, this proposal introduces a new form of the operators for
which it is correct (even expected) to be a function template. To that
end, we allow a templated operator delete
to be considered a usual deallocation function, as long as the only
dependently-typed parameter is the first std::type_identity<T>
parameter. To our minds, these semantics match the “intent” of the
restrictions already in place for the other implicit parameters like
std::align_val_t.
The cost of not allowing a templated type-aware operator delete
as a usual deallocation function is very high, as it functionally
prohibits the use of type-aware allocation operators in any environment
that requires the ability to clean up after a constructor has thrown an
exception.
We have decided not to support type-aware destroying delete as we believe it creates a user hazard. At a technical level there is no additional complexity in supporting type-aware destroying delete, but the resulting semantics seem likely to cause a lot of confusion. For example, given this hypothetical declaration:
struct Foo {
// …
template <class T>
void operator delete(std::type_identity<T>, Foo*, std::destroying_delete_t);
};
struct Bar : Foo { };
void f(Foo* foo) {
delete foo; // calls Foo::operator delete<Foo>
}
void g(Bar *bar) {
delete bar; // calls Foo::operator delete<Bar>
}To a user this appears to be doing what they expect. However, consider the following:
struct Oops : Bar { };
void h(Oops *oops) {
g(oops); // calls Foo::operator delete<Bar> from within g
}By design, destroying delete does not perform any polymorphic dispatch, and as a result the type being passed to the operator is not be the dynamic type of the object being destroyed, but rather its static type. As a result, basic functionality will appear to work correctly from the user’s point of view when in reality the rules are much subtler than they seem.
Given that the design intent of destroying delete is for users to manage destruction and dispatching manually, we believe that adding type-awareness to destroying delete will add little value while creating the potential for confusion, so we decided not to do it.
The initial proposal allowed the specification of type-aware operators in namespaces that would then be resolved via ADL. Upon further consideration, this introduces a number of challenges that are difficult to resolve robustly. As a result, we have dropped support for namespace-scope operator declarations and removed the use of ADL from the proposal.
The first problem is that ADL would be based on the type of all
arguments passed to operator new,
including placement arguments. While this is not a problem for operator new
itself, operator delete
does not get the same placement arguments, which would potentially
change the set of associated namespaces used to resolve
new and
delete.
One of our original motivations for allowing namespace-scoped
operators was to simplify the task of providing operators for a whole
library. However, since ADL is so viral, the set of associated
namespaces can easily grow unintentionally (e.g. new lib1::Foo<lib2::Bar>(...)),
which means that developers would have to appropriately constrain their
type-aware operators anyway. In other words, we believe that a
declaration like this would never have been a good idea in the first
place:
namespace lib {
// intent: override for all types in this namespace
template <class T>
void* operator new(std::type_identity<T>, std::size_t);
}There are too many ways in which an unconstrained declaration like
this can break, including an unexpected set of associated namespaces or
even a mere using namespace lib;.
Given the need to constrain a type-aware operator anyway, we believe that allowing namespace-scoped operators is merely a nice-to-have but not something that we need fundamentally. Furthermore, adding this capability to the language could always be pursued as a separate proposal since that concern can be tackled orthogonally. For example, a special ADL lookup could be done based solely on the dynamic type being [de]allocated.
Since this adds complexity to the proposal and implementation and doesn’t provide great value, we are not pursuing it as part of this proposal.
Non-type-aware allocation calls are evaluated by trial overload resolution using the concatenation of a set of implicit arguments with the arguments passed with the placement-new syntax. Developers make use of this to support manually allocating overaligned storage for a non-alignment-extended type, e.g
new (std::align_val_t(512)) int; // allocate storage for a single int, aligned to 512 bytesThis is a useful and used feature, which we have found needs
to be supported in practice, but the wording changes post-Hagenberg
require this function to be explicitly described rather than as an
implicit consequence of the existing trial based operator new
lookup.
Type-aware allocation function support this use case by discarding
the implicit alignment argument if the first placement argument is of
type std::align_val_t
when calling the allocation function.
delete
even if type-aware placement delete existsThis proposal initially intended to make deleting an object
ill-formed if, when delete an object of type
U there are no usual type-aware
deallocation functions with a type-identity parameter of type std::type_identity<U>,
but there are placement type-aware deallocation functions that
do have a type-identity parameter of type std::type_identity<U>.
struct S {
S();
void *operator new(std::type_identity<S>, size_t, std::align_val_t, placement_t); // #1
void operator delete(std::type_identity<S>, void *, size_t, std::align_val_t, placement_t); // #2
void operator delete(void *); // #3
};
...
S* obj = new (placement{}) S;
delete obj;
...The new expression resolves operator new
at #1 to perform the allocation, and resolves the operator delete
at #2 to clean up if the constructor throws an exception. But when
delete is called we produce an error: Both operator delete
declarations are found, the only candidate that can be used for a delete
expression is the non-type aware declaration at like #3, however because
we found the placement type-aware operator delete
and it accepts a type-identity argument of type std::type_identity<S>.
The fix for this error is to introduce a usual type-aware deallocator
struct S {
. . .
void operator delete(std::type_identity<S>, void *, size_t, std::align_val_t);
};The original intention was to reject this as a hazard reduction:
preventing errors in which a developer incorrectly specified a
type-aware deallocation function, e.g. the size and
alignment parameters of developer specified sized and aligned
operator delete
are frequently reversed.
Following Hagenberg these optional parameters became mandatory for type-aware allocation and deallocation functions, making errors of this kind impossible.
At the same time, relaxing this requirement provides a much more ergonomic path for the common case where object type is not considered important during deallocation, as the non-type aware operator delete is a valid deallocation function for such types. e.g the common case for such a deallocator is simply
template <class T> void operator delete(std::type_identity<T>, void *ptr, size_t, std::align_val_t) {
untyped_free(ptr);
}Lifting this restriction does not remove the requirement that both allocation and deallocation functions are declared in the same scope.
std::type_identityThe proposal disallows the use of a dependent type to reference std::type_identity,
by design this prevents the use of alias templates, as there is no
functional difference between an alias template and any other template
for the purpose of declaration site validation. This restriction is
consistent with existing specification’s definition for the
size and
address parameters, and the return
type.
std::allocator<T>Today, std::allocator<T>::allocate
is specified to call ::operator new(std::size_t)
explicitly. Even if T::operator new
exists, std::allocator<T>
will not attempt to call it. We view this as a defect in the current
standard since std::allocator<T>
could instead select the same operator that would be called in an
expression like new T(...)
(without the constructor call, obviously).
This doesn’t have an interaction with our proposal, except for making
std::allocator<T>’s
behavior a bit more unfortunate than it already is today. Indeed, users
may rightly expect that std::allocator<T>
will call their type-aware operator new
when in reality that won’t be the case.
Since this deception already exists for T::operator new,
we do not attempt to change std::allocator<T>’s
behavior in this proposal. However, the authors are willing to
investigate fixing this issue as a separate proposal, which will
certainly present its own set of challenges (e.g. constant
evaluation).
operator new
/ operator deleteA concern that was raised in St-Louis was that this proposal would
increase the likelihood of ODR violation caused by different
declarations of operator new/operator delete
being used in different TUs. For example, one TU would get lib1::operator new
and another TU would use lib2::operator delete
due to e.g. a different set of headers being included. Note that the
exact same issue also applies to every other operator that is commonly
used via ADL (like operator+),
except that many such ODR violations may end up being more benign than a
mismatched
new/delete.
First, we believe that the only way to avoid this issue (in general) is to properly constrain templated declarations, and nothing can prevent users from doing that incorrectly. However, since this proposal has dropped the ADL lookup, declarations of type-aware operators must now be in-class or global. This greatly simplifies the selection of an operator, which should make it harder for users to unexpectedly define an insufficiently constrained operator without immediately getting a compilation error.
Furthermore, without ADL lookup, the ODR implications of this proposal are exactly the same as the existing ODR implications of user-defined placement new operators, which can be templates.
This proposal does not have any impact on the library, since this
only tweaks the search process performed by the compiler when it
evaluates a new-expression and a delete-expression. In particular, we do
not propose adding new type-aware free function operator new
variants in the standard library at this time, althought this could be
investigated in the future.
Coroutines currently allow using a custom operator new
and operator delete
for allocating the coroutine frame. That is done by looking up in the
coroutine’s promise type for Promise::operator new.
However, there is no mechanism to communicate the type being allocated,
since that type is only something known by the compiler (and fairly late
during translation). This paper does not propose changing how allocation
for coroutine frames is customized. In the future, the coroutine
specification could be updated to make the type being allocated more
visible and to use a typed allocation function, but there is significant
enough design space to avoid doing that here.
1 Objects can be created dynamically during program execution, using new-expressions ([expr.new]), and destroyed using delete-expressions ([expr.delete]). An allocation function is a function or function template named
operator neworoperator new[]. A deallocation function is a function or function template namedoperator deleteoroperator delete[]. A C++ implementation provides access to, and management of, dynamic storage via the globalallocation functionsallocation andoperator newandoperator new[]the global deallocation functionsdeallocation functions.operator deleteandoperator delete[]2 The library provides default definitions for the global allocation and deallocation functions. Some global allocation and deallocation functions are replaceable ([dcl.fct.def.replace]). The following allocation and deallocation functions ([support.dynamic]) are implicitly declared in global scope in each translation unit of a program.
[. . .]
Allocation and/or deallocation functions may also be declared and defined for any class ([class.free]).
2a The first parameter P of an allocation or deallocation function is the type-identity parameter if the type U of P is a specialization of
std::type_identity([meta.trans.other]) and, if U is dependent, U has the shapeTT<T>, whereTis a type andTTdenotesstd::type_identityand is not dependent. An allocation function that has a type-identity parameter is a type-aware allocation function. A deallocation function that has a type-identity parameter is a type-aware deallocation function.[Example:— end example ]template <typename T> using A = std::type_identity<T>; template <typename U> void* operator new(A<U>, std::size_t, std::align_val_t); // not a type-aware allocation function template<typename T> struct W { using B = std::type_identity<T>; }; template <typename U> void* operator new(W<U>::B, std::size_t, std::align_val_t); // not a type-aware allocation function2b If a class scope S is the target scope of a type-aware allocation function F, then
- (2b.1) if the name of F is
operator neworoperator delete, S shall be the target scope of an allocation function namedoperator newand a deallocation function namedoperator delete, or- (2b.2) if the name of F is
operator new[]oroperator delete[], S shall be the target scope of an allocation function namedoperator new[]and a deallocation function namedoperator delete[].3 If the behavior of an allocation or deallocation function does not satisfy the semantic constraints specified in [basic.stc.dynamic.allocation] and [basic.stc.dynamic.deallocation], the behavior is undefined.
1 An allocation function
that is not a class member functionshall either be a class member or belong to the global scope and not have a name with internal linkage.1a The declared return type of an allocation function F shall be the non-dependent type “pointer to
void”. The minimum number of parameters of F is determined as follows:
- (1a.1) If F is a type-aware allocation function, then F shall have at least three parameters, where the second parameter is called the size parameter and the third parameter is called the alignment parameter.
- (1a.2) Otherwise, if F is a function template, then F shall have at least two parameters, where the first parameter is called the size parameter.
- (1a.3) Otherwise F shall be a function that has at least one parameter, where the first parameter is called the size parameter.
The following restrictions apply to the parameters of F:
- (1a.4) The type-identity parameter shall not have a default argument.
- (1a.5) The
firstsize parameter shall have the non-dependent typestd::size_t([support.types]). The first parameterand shall not have a default argument.- (1a.6) The alignment parameter shall have the non-dependent type
std::align_val_t([support.dynamic]) and shall not have a default argument.
The value of the first parameter is interpreted as the requested size of the allocation. An allocation function can be a function template. Such a template shall declare its return type and first parameter as specified above (that is, template parameter types shall not be used in the return type and first parameter type). Allocation function templates shall have two or more parameters.[Note: The value of the size parameter is interpreted as the requested size of the allocation. The value of the alignment parameter is interpreted as the alignment requirement of the allocation. The template argument of the type-identity parameter is interpreted as the type of the complete object that will use the allocation as its storage. An allocation function can have additional parameters that are not specified in this document. — end note ]
2 An allocation function attempts to allocate the
requestedamount of storage specified by the size parameter. If it is successful, it returns the address of the start of a block of storage whose length in bytes is at least as large as the requested size. The order, contiguity, and initial value of storage allocated by successive calls to an allocation function are unspecified. Even if the size of the space requested is zero, the request can fail. If the request succeeds, the value returned by a replaceable allocation function is a non-null pointer value ([basic.compound])p0different from any previously returned valuep1, unless that valuep1was subsequently passed to a replaceable deallocation function. Furthermore, for the library allocation functions in [new.delete.single] and [new.delete.array],p0represents the address of a block of storage disjoint from the storage for any other object accessible to the caller. The effect of indirecting through a pointer returned from a request for zero size is undefined.18
[ Drafting note: The new wording for the return type of allocation
and deallocation functions might resolve CWG1676
“auto return type for allocation
and deallocation functions”, as it follows the approach in CWG1669
“auto return type for
main”. ]
1 A deallocation function
that is not a class member functionshall either be a class member or belong to the global scope and not have a name with internal linkage.
Apply the following changes to paragraph 2 and move it after the paragraph 3:
2 A usual deallocation function is a destroying operator delete if
it has at least two parametersits first parameter is of type “pointer toC”, whereCis a class type, and its second parameter is of typestd::destroying_delete_t([new.syn]).A destroying operator delete shall be a class member function namedA destroying operator delete shall belong to a class scope introduced byoperator delete.C.[Note: Array deletion cannot use a destroying operator delete. — end note ]
[Note: A destroying operator delete cannot be a function template or a type-aware deallocation function, because it cannot have a type-identity parameter. — end note ]
Insert a new paragraph before paragraph 3 and apply the following changes to paragraph 3:
3- The declared return type of a deallocation function F shall be the non-dependent type
void. The following restrictions apply to the parameters of F:
- (3-.1) If F is a type-aware deallocation function, then F shall have at least four parameters, where
- (3-.1.1) the second parameter is called the address parameter,
- (3-.1.2) the third parameter shall have the non-dependent type
std::size_t([support.types]), and- (3-.1.3) the fourth parameter shall have the non-dependent type
std::align_val_t([support.dynamic]).- (3-.2) Otherwise, if F is a function template, then F shall have at least two parameters, where the first parameter is called the address parameter.
- (3-.3) Otherwise, F shall be a function that has at least one parameter, where the first parameter is called the address parameter.
The address parameter of a deallocation function that is not a destroying operator delete (see below) shall have the non-dependent type “pointer to
void”.[Note: A deallocation function can have additional parameters that are not specified in this document. — end note ]
3
Each deallocation function shall returnvoid.If the function is a destroying operator delete declared in class type C, the type of its first parameter shall be “pointer to C”; otherwise, the type of its first parameter shall be “pointer to void”.A deallocation function may have more than one parameter.A usual deallocation function is
- (3.1) a type-aware deallocation function with four parameters or
- (3.2) a deallocation function which is not a function template or function template specialization and whose parameters after the
firstaddress parameter
A destroying operator delete shall be a usual deallocation function.A deallocation function may be an instance of a function template.Neither the first parameter nor the return type shall depend on a template parameter.A deallocation function template shall have two or more function parameters.A template instance is never a usual deallocation function, regardless of its signature.
[ Drafting note: The new wording for the return type of deallocation
functions might resolve CWG3025
“Deallocation functions returning
void”. ]
19 The new-placement syntax is used to supply additional arguments to an allocation function; such an expression is called a placement new-expression.
20
Overload resolution is performed on a function call created by assembling an argument list. The first argument is the amount of space requested, and has typestd::size_t. If the type of the allocated object has new-extended alignment, the next argument is the type’s alignment, and has typestd::align_val_t. If the new-placement syntax is used, the initializer-clauses in its expression-list are the succeeding arguments. If no matching function is found then
- (20.1)
if the allocated object type has new-extended alignment, the alignment argument is removed from the argument list;- (20.2)
otherwise, an argument that is the type’s alignment and has typestd::align_val_tis added into the argument list immediately after the first argument;
and then overload resolution is performed again.Let
- (20.1) T be a value of type
std::type_identity<U>([meta.trans.other]), whereUis a cv-unqualified version of the array element type if the allocated type is an array type, and a cv-unqualified version of the allocated type otherwise,- (20.2) S be an expression of type
std::size_tequal to the amount of space requested,- (20.3) A be an expression of type
std::align_val_tequal to the alignment of the allocated type, and- (20.4) P be the initializer-clauses in the expression-list of the new-placement if the new-placement syntax is used, and an empty set otherwise.
An allocation function is selected by performing overload resolution, where the candidate list is the result of the search for the allocation function’s name, and the argument list consists of
- (20.5) (T, S, A, P) if P is an empty set or the first initializer-clause in P does not have the type
std::align_val_tafter parameter transformations ([dcl.fct]), and (T, S, P) otherwise, or, if overload resolution fails,- (20.6) (S, A, P) if the type of the allocated object has new-extended alignment, and (S, P) otherwise, or, if overload resolution fails,
- (20.7) (S, P) if the type of the allocated object has new-extended alignment, and (S, A, P) otherwise.
[Example:
(21.1)
new Tresults in one of the following calls:operator new(std::type_identity<std::remove_cv_t<T>>(), sizeof(T), std::align_val_t(alignof(T)))operator new(sizeof(T)) operator new(sizeof(T), std::align_val_t(alignof(T)))(21.2)
new(2,f) Tresults in one of the following calls:operator new(std::type_identity<std::remove_cv_t<T>>(), sizeof(T), std::align_val_t(alignof(T)), 2, f)operator new(sizeof(T), 2, f) operator new(sizeof(T), std::align_val_t(alignof(T)), 2, f)(21.3)
new T[5]results in one of the following calls:operator new[](std::type_identity<std::remove_cv_t<T>>(), sizeof(T) * 5 + x, std::align_val_t(alignof(T)))operator new[](sizeof(T) * 5 + x) operator new[](sizeof(T) * 5 + x, std::align_val_t(alignof(T)))(21.4)
new(2,f) T[5]results in one of the following calls:operator new[](std::type_identity<std::remove_cv_t<T>>(), sizeof(T) * 5 + x, std::align_val_t(alignof(T)), 2, f)operator new[](sizeof(T) * 5 + x, 2, f) operator new[](sizeof(T) * 5 + x, std::align_val_t(alignof(T)), 2, f)
(21.5)
new(std::align_val_t(7)) Tresults in one of the following calls:operator new(std::type_identity<std::remove_cv_t<T>>(), sizeof(T), std::align_val_t(7)) operator new(sizeof(T), std::align_val_t(alignof(T)), std::align_val_t(7)) operator new(sizeof(T), std::align_val_t(7))(21.6)
new(std::align_val_t(7), f) Tresults in one of the following calls:operator new(std::type_identity<std::remove_cv_t<T>>(), sizeof(T), std::align_val_t(7), f) operator new(sizeof(T), std::align_val_t(alignof(T)), std::align_val_t(7), f) operator new(sizeof(T), std::align_val_t(7), f)(21.7)
new(f, std::align_val_t(7)) Tresults in one of the following calls:operator new(std::type_identity<std::remove_cv_t<T>>(), sizeof(T), std::align_val_t(alignof(T)), f, std::align_val_t(7)) operator new(sizeof(T), std::align_val_t(alignof(T)), f, std::align_val_t(7)) operator new(sizeof(T), f, std::align_val_t(7))(21.8)
new(std::align_val_t(7)) T[5]results in one of the following calls:operator new[](std::type_identity<std::remove_cv_t<T>>(), sizeof(T) * 5 + x, std::align_val_t(7)) operator new[](sizeof(T) * 5 + x, std::align_val_t(alignof(T)), std::align_val_t(7)) operator new[](sizeof(T) * 5 + x, std::align_val_t(7))(21.9)
new(std::align_val_t(7), f) T[5]results in one of the following calls:operator new[](std::type_identity<std::remove_cv_t<T>>(), sizeof(T) * 5 + x, std::align_val_t(7), f) operator new[](sizeof(T) * 5 + x, std::align_val_t(alignof(T)), std::align_val_t(7), f) operator new[](sizeof(T) * 5 + x, std::align_val_t(7), f)(21.10)
new(f, std::align_val_t(7)) T[5]results in one of the following calls:operator new[](std::type_identity<std::remove_cv_t<T>>(), sizeof(T) * 5 + x, std::align_val_t(alignof(T)), f, std::align_val_t(7)) operator new[](sizeof(T) * 5 + x, std::align_val_t(alignof(T)), f, std::align_val_t(7)) operator new[](sizeof(T) * 5 + x, f, std::align_val_t(7))Here, each instance of
xis a non-negative unspecified value representing array allocation overhead; the result of the new-expression will be offset by this amount from the value returned byoperator new[]. This overhead may be applied in all array new-expressions, including those referencing a placement allocation function, except when referencing the library functionoperator new[](std::size_t, void*). The amount of overhead may vary from one invocation of new to another. — end example ][. . .]
28 If the new-expression does not begin with a unary
:: operator and the allocated type is a class typeTor an array thereof, a search is performed for the deallocation function’s name in the scope ofT. Otherwise, or if nothing is found, the deallocation function’s name is looked up by searching for it in the global scope.29 For a non-placement allocation function, the normal deallocation function lookup is used to find the matching deallocation function ([expr.delete]). For a placement allocation function, the selection process is described below. In any case, the matching deallocation function (if any) shall be non-deleted and accessible from the point where the new-expression appears.
30 For a placement allocation function, the matching deallocation function is selected as follows:
(30.1) Each candidate that is a function template is replaced by the function template specializations (if any) generated using template argument deduction ([temp.over], [temp.deduct.call]) with arguments as specified below.
(30.2) Each candidate whose associated constraints (if any) are not satisfied ([temp.constr.constr]) is removed from the set of candidates.
(30.2a) If the selected allocation function is a type-aware allocation function, overload resolution shall select a deallocation function F with arguments as specified below and deallocation functions as the set of candidates. The parameter-type-list of F shall be identical to that of the allocation function, when considering parameters after their respective alignment parameters. F is selected as the deallocation function.
(30.3) Each candidate whose parameter-type-list is not identical to that of the allocation function, ignoring their respective first parameters, is removed from the set of candidates.
(30.4) If exactly one function remains, that function is selected.
(30.5) Otherwise, no deallocation function is selected.
If a usual deallocation function is selected, the program is ill-formed.
[Example: [. . .] — end example ]
32
If a new-expression calls a deallocation function, it passes the value returned from the allocation function call as the first argument of typeThe argument list for the deallocation function is the same as that of the selected allocation function, except that the value returned from the allocation function call is insertedvoid*. If a placement deallocation function is called, it is passed the same additional arguments as were passed to the placement allocation function, that is, the same arguments as those specified with the new-placement syntax.
- (32.1) after the first argument, if the selected allocation function is a type-aware allocation function,
- (32.2) before the first argument otherwise.
If the implementation is allowed to introduce a temporary object or make a copy of any argument as part of the call to the allocation function, it is unspecified whether the same object is used in the call to both the allocation and deallocation functions.
8 If the keyword
deletein a delete-expression is not preceded by the unary:: operator and the type of the operand is a pointer to a (possibly cv-qualified) class typeTor (possibly multidimensional) array thereof:
- (8.1) For a single-object delete expression, if the operand is a pointer to cv
TandThas a virtual destructor, the deallocation function is the one selected at the point of definition of the dynamic type’s virtual destructor ([class.dtor]).- (8.2) Otherwise, a search is performed for the deallocation function’s name in the scope of
T.Otherwise, or if nothing is found, the deallocation function’s name is looked up by searching for it in the global scope. In any case, any declarations other than of usual deallocation functions ([basic.stc.dynamic.deallocation]) are discarded.
[Note: If only a placement deallocation function is found in a class, the program is ill-formed because the lookup set is empty ([basic.lookup]). — end note ]
9 The deallocation function to be called for an operand of type “pointer to cv
U” is selected as follows:
- (9.1) If any of the deallocation functions is a destroying operator delete, all deallocation functions that are not destroying operator deletes are eliminated from further consideration.
- (9.1a) If any of the deallocation functions is a type-aware deallocation function:
- (9.1a.1) Let L be a list of arguments for a function call that consists of
- (9.1a.1.1) a value of type
std::type_identity<U>([meta.trans.other]), called the type-identity argument,- (9.1a.1.2) an unspecified value of type “pointer to
void”,- (9.1a.1.3) an unspecified value of type
std::size_t, and- (9.1a.1.4) an unspecified value of type
std::align_val_t.- (9.1a.2) Each deallocation function that is a function template is replaced by the function template specializations (if any) generated using template argument deduction ([temp.over], [temp.deduct.call]) with L as the argument list.
- (9.1a.3) If any of the deallocation functions have the first parameter of the type
std::type_identity<U>, then overload resolution shall select the deallocation function using L as the argument list and the deallocation functions as the set of candidates, and the selection process terminates. Otherwise, type-aware deallocation functions are eliminated from further consideration.- (9.2) If the type has new-extended alignment, a function with a parameter of type
std::align_val_tis preferred; otherwise a function without such a parameter is preferred. If any preferred functions are found, all non-preferred functions are eliminated from further consideration.- (9.3) If exactly one function remains, that function is selected and the selection process terminates.
- (9.4) If the deallocation functions belong to a class scope, the one without a parameter of type
std::size_tis selected.- (9.5) If the type is complete and if, for an array delete expression only, the operand is a pointer to a class type with a non-trivial destructor or a (possibly multidimensional) array thereof, the function with a parameter of type
std::size_tis selected.- (9.6) Otherwise, it is unspecified whether a deallocation function with a parameter of type
std::size_tis selected.Unless the deallocation function is selected at the point of definition of the dynamic type’s virtual destructor, the selected deallocation function shall be accessible from the point where the delete-expression appears.
10 For a single-object delete expression, the deleted object is the object A pointed to by the operand if the static type of A does not have a virtual destructor, and the most-derived object of A otherwise.
[Note: If the deallocation function is not a destroying operator delete and the deleted object is not the most derived object in the former case, the behavior is undefined, as stated above. — end note ]
For an array delete expression, the deleted object is the array object. When a delete-expression is executed, the selected deallocation function shall be called with the address of the deleted object in a single-object delete expression, or the address of the deleted object suitably adjusted for the array allocation overhead ([expr.new]) in an array delete expression, as
its first argumentthe argument corresponding to the address parameter.[Note: Any cv-qualifiers in the type of the deleted object are ignored when forming this argument. — end note ]
If a destroying operator delete is used, an unspecified value is passed as the argument corresponding to the parameter of type
std::destroying_delete_t. If the selected function is a type-aware deallocation function, the type-identity argument is passed as the argument corresponding to the type-identity parameter. If a deallocation function with a parameter of typestd::align_val_tis used, the alignment of the type of the deleted object is passed as the corresponding argument. If a deallocation function with a parameter of typestd::size_tis used, the size of the deleted object in a single-object delete expression, or of the array plus allocation overhead in an array delete expression, is passed as the corresponding argument.[Note: If this results in a call to a replaceable deallocation function, and either the first argument was not the result of a prior call to a replaceable allocation function or the second or third argument was not the corresponding argument in said call, the behavior is undefined ([new.delete.single], [new.delete.array]). — end note ]
[ Drafting note: It’s not entirely clear that “In any case” in [expr.delete]/8 applies to the entire paragraph. ]
2 An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:
- [. . .]
- (2.19) a new-expression ([expr.new]), unless either
- (2.19.1) the selected allocation function is a replaceable global allocation function ([new.delete.single], [new.delete.array]) and the allocated storage is deallocated within the evaluation of E, or
- (2.19.1a) the selected allocation function is a type-aware allocation function ([basic.stc.dynamic.general]) that belongs to the global scope and whose parameter-type-list, ignoring the type-identity parameter, matches parameter-type-list of a replaceable global allocation function, or
- (2.19.2) the selected allocation function is a non-allocating form ([new.delete.placement]) with an allocated type
T, where
- (2.19.2.1) the placement argument to the new-expression points to an object that is transparently replaceable ([basic.life]) by the object created by the new-expression or, if T is an array type, to the first element of such an object, and
- (2.19.2.2) the placement argument points to storage whose duration began within the evaluation of E;
- [. . .]
Update [tab:cpp.predefined.ft]
to include a feature-test macro
__cpp_typed_allocation with a
release-appropriate value.
#define __cpp_typed_allocation 202XXXL
We’d like to acknowledge the time spent by Jens Maurer, Brian Bi, Corentin Jabot, Richard Smith, Hana Dusikova, Lauri Vasama, and others to help us develop the wording in this proposal.