We need to separate allocation and initialization of some heap storage. Unfortunately, client code uses delete p;
to delete the pointer. If we had control of the deletion, we could allocate with ::operator new (sizeof(T), std::align_val_t(alignof(T)))
and then use placement new
and the corresponding global ::operator delete
. But (if I understand correctly) delete
might call T::operator delete
, potentially requiring an allocation that came from T::operator new
, with a complicated preference order between the different possible arguments for T::operator new
and T::operator delete
if there are multiple overloads.
Is there something like std::allocate_as_if_by_new<T>()
that will do exactly the same allocation that new T(...)
would do, but without initializing, or at least guarantee that (after initialization) delete p;
will correctly deallocate it?
Short answer: No Yes, as long as you're very careful that the appropriate deallocation function will be called.
Long answer:
What pointers are valid operands for single-object delete expressions (e.g. delete ptr;
) is covered in the standard by expr.delete
:
7.6.2.9 Unary Expressions - Delete [expr.delete] [emphasis mine]
(2) [...] In a single-object delete expression, the value of the operand of delete may be a null pointer value, a pointer to a non-array object created by a previous new-expression, or a pointer to a subobject representing a base class of such an object. If not, the behavior is undefined. [...]
So as long as the pointer comes from any new expression (which could also potentially be a new-placement expression) it should be well-defined.
You do have to watch out though that delete ptr;
will actually call a deallocation function that can properly deallocate the storage - which requires a bit of work.
There are three gotchas that you'll need to look out for:
This will not work for the array versions of new & delete (i.e. new T[]
/ delete[] ptr
)
The array delete expression has a subclause that explicitly disallows new-placement:
7.6.2.9 Unary Expressions - Delete [expr.delete] [emphasis mine]
(2) [...] In an array delete expression, the value of the operand of delete may be a null pointer value or a pointer value that resulted from a previous array new-expression whose allocation function was not a non-allocating form ([new.delete.placement]). If not, the behavior is undefined.
class-types can provide custom implementations for operator new
/ operator delete
as members (11.4.11 Class members - Allocation and deallocation functions [class.free]), which need to be preferred over the global allocation functions if they're present
Depending on wether or not T
has new-extended alignment (i.e. alignof(T) > __STDCPP_DEFAULT_NEW_ALIGNMENT__
), the overload of operator new
/ operator delete
with the std::align_val_t
parameter needs to be checked first.
This is necessary because operator delete(void* [, std::size_t])
can't be used to free memory allocated by operator new(std::size_t, std::align_val_t)
(and vice-versa with operator delete(void* [, std::size_t], std::align_val_t)
and operator new(std::size_t)
)
This is mandated by the precondition on operator delete
:
17.6.3.2 Storage allocation and deallocation - Single-object forms [new.delete.single]
void operator delete(void* ptr) noexcept; void operator delete(void* ptr, std::size_t size) noexcept; void operator delete(void* ptr, std::align_val_t alignment) noexcept; void operator delete(void* ptr, std::size_t size, std::align_val_t alignment) noexcept;
(10) Preconditions: [...]
(11) If the alignment parameter is not present, ptr was returned by an allocation function without an alignment parameter. If present, the alignment argument is equal to the alignment argument passed to the allocation function that returned ptr. [...]
Which basically forces you to either:
operator new
and operator delete
with a std::align_val_t
(passsing the same value)std::align_val_t
parameterSo you need to mimic the same allocation that a non-placement new-expression would make, so that delete ptr
calls the matching operator delete
.
This is covered in 7.6.2.8 New [expr.new], and basically boils down to one of two calls:
operator new(sizeof(T))
operator new(sizeof(T), std::align_val_t(alignof(T)))
T
is a class-type then overload resolution will first be attempted within T
(and only if that fails in the global scope)T
requires new-extended alignment then the std::align_val_t
overload will be tried first, otherwise the non-std::align_val_t
overload.So a function like allocate_as_if_by_new<T>
would need to be implemented like this to properly allocate storage as a non-placement new-expression would do:
template<class T, class... Args>
concept has_class_new = requires() {
{ T::operator new(std::declval<Args>()...) } -> std::same_as<void*>;
};
template<class T, class... Args>
concept has_global_new = requires() {
{ ::operator new(std::declval<Args>()...) } -> std::same_as<void*>;
};
template<class T>
requires(std::is_object_v<T> && !std::is_array_v<T>)
void* allocate_as_if_by_new() {
// class-type T's may have overloaded operator new / operator delete
if constexpr(std::is_class_v<T> || std::is_union_v<T>) {
if constexpr(alignof(T) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
// T has new-extended alignment
// => std::align_val_t overloads need to be checked first
if constexpr(has_class_new<T, std::size_t, std::align_val_t>)
return T::operator new(sizeof(T), std::align_val_t(alignof(T)));
else if constexpr(has_class_new<T, std::size_t>)
return T::operator new(sizeof(T));
} else {
// T has an alignment less than or equal to __STDCPP_DEFAULT_NEW_ALIGNMENT__
// => prefer overloads without std::align_val_t
if constexpr(has_class_new<T, std::size_t>)
return T::operator new(sizeof(T));
else if constexpr(has_class_new<T, std::size_t, std::align_val_t>)
return T::operator new(sizeof(T), std::align_val_t(alignof(T)));
}
}
// either not a class-type, or a class-type without overloaded operator new
// note that we still need to check for both overloads of operator new in both cases.
// a program might only define a replacement for one of those functions,
// which would cause the other one to be not defined.
if constexpr(alignof(T) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
if constexpr(has_global_new<T, std::size_t, std::align_val_t>)
return ::operator new(sizeof(T), std::align_val_t(alignof(T)));
else if constexpr(has_global_new<T, std::size_t>)
return ::operator new(sizeof(T));
} else {
if constexpr(has_global_new<T, std::size_t>)
return ::operator new(sizeof(T));
else if constexpr(has_global_new<T, std::size_t, std::align_val_t>)
return ::operator new(sizeof(T), std::align_val_t(alignof(T)));
}
// should never happen
throw std::bad_alloc();
}
Usage:
void* ptr = allocate_as_if_by_new<Foo>();
// ...
Foo* foo = new(ptr) Foo();
// ...
delete foo;
There are a few alternatives you might consider:
Allocators split allocation and construction into separate steps (and destruction & deallocation).
So assuming you can change your code to use std::allocator
(or any other allocator that satisfies the allocator completeness requirements) then it would be possible to allocate T
's without initializing them:
template<class T>
void example() {
using alloc_traits = std::allocator_traits<std::allocator<T>>;
std::allocator<T> alloc;
T* ptr = alloc_traits::allocate(alloc, 1);
// ptr is uninitialized
// ...
// construct a T at ptr
alloc_traits::construct(alloc, ptr /* , ...constructor args */);
// ...
// "delete ptr;" replacement:
alloc_traits::destroy(alloc, ptr);
alloc_traits::deallocate(alloc, ptr, 1);
}
or given that you tagged your question with unique-ptr:
template<class T>
struct std_allocator_deleter {
constexpr void operator()(T* ptr) const {
using alloc_traits = std::allocator_traits<std::allocator<T>>;
std::allocator<T> alloc;
alloc_traits::destroy(alloc, ptr);
alloc_traits::deallocate(alloc, ptr, 1);
}
};
template<class T>
std::unique_ptr<T, std_allocator_deleter<T>> allocate_unique() {
using alloc_traits = std::allocator_traits<std::allocator<T>>;
std::allocator<T> alloc;
T* ptr = alloc_traits::allocate(alloc, 1);
return std::unique_ptr<T, std_allocator_deleter<T>>(ptr);
}
template<class T>
void example() {
// allocate uninitialized storage
auto ptr = allocate_unique<T>();
// ...
// construct a T
new(ptr.get()) T();
// ...
// ptr destructor automatically destroys & deallocates T
}
Note that you must ensure that you always create a new T
object in that storage, otherwise the std_allocator_deleter
deleter would have undefined behavior.
A simple fix would be to just not use a delete-expression.
If you explicitly call the destructor followed by a call to operator delete
your program would also be well behaved:
T* ptr = (T*)::operator new(sizeof(T));
// ...
new(ptr) T();
// ...
// maybe ok, maybe undefined behavior (depening on alignment requirements of T and if T has overloads for operator new / delete)
// delete ptr;
// always ok:
ptr->~T();
::operator delete(ptr);
If your T
types are cheaply default constructible (or you can modify them to be so) then you could just use a normal new-expression and call its destructor immediately:
template<class T>
T* allocate_uninitialized() {
T* ptr = new T;
ptr->~T();
return ptr;
}
template<class T>
void example() {
T* ptr = allocate_unitialized<T>();
// ...
new (ptr) T();
// ...
delete ptr;
}
Note that you must always place a new T
object at the pointer returned by allocate_uninitialized()
before you pass it to a delete-expression, otherwise the behavior is undefined.