Is it OK to make a placement new on memory managed by a smart pointer?

  • A+
Category:Languages

Context

For test purpose, I need to construct an object on non-zero memory. This could be done with:

{     struct Type { /* IRL not empty */};     std::array<unsigned char, sizeof(Type)> non_zero_memory;     non_zero_memory.fill(0xC5);     auto const& t = *new(non_zero_memory.data()) Type;     // t refers to a valid Type whose initialization has completed.     t.~Type(); } 

Since this is tedious and made multiple times, I'd like to provide a function returning a smart pointer to such a Type instance. I came up with the following, but I fear undefined behavior lurk somewhere.

Question

Is the following program well defined? Especially, is the fact that a std::byte[] has been allocated but a Type of equivalent size is freed an issue?

#include <cstddef> #include <memory> #include <algorithm>  auto non_zero_memory(std::size_t size) {     constexpr std::byte non_zero = static_cast<std::byte>(0xC5);      auto memory = std::make_unique<std::byte[]>(size);     std::fill(memory.get(), memory.get()+size, non_zero);     return memory; }  template <class T> auto on_non_zero_memory() {     auto memory = non_zero_memory(sizeof(T));     return std::shared_ptr<T>(new (memory.release()) T()); }      int main() {     struct Type { unsigned value = 0; ~Type() {} }; // could be something else     auto t = on_non_zero_memory<Type>();     return t->value; } 

Live demo

 


The rule is that if a type has a trivial destructor, you don't need to call it. So, this:

return std::shared_ptr<T>(new (memory.release()) T()); 

is almost well formed. It omits the destructor of the sizeof(T) std::bytes, which is fine, constructs a new T in the memory, which is fine, and then the destructor for std::shared_ptr calls the matching destructor for T that was constructed. However, it doesn't call delete[] for the std::byte array.

Another problem is the memleak if the constructor of T throws. A simple fix is to just make it delete[] and construct T before the unique pointer is released (So delete[] is called):

T* ptr = new (memory.get()) T(); memory.release(); return std::shared_ptr<T>(ptr, [](T* ptr) {     ptr->~T();     delete[] reinterpret_cast<std::byte*>(ptr); }); 

EDIT:

Quotes from the standard:

Section [new.delete.array] (21.6.2.2)

void operator delete[](void* ptr) noexcept;
11. Requires: ptr shall be a null pointer or its value shall represent the address of a block of memory allocated by an earlier call to a (possibly replaced) operator new[](std::size_t) or operator new[](std::size_t, std::align_val_t) which has not been invalidated by an intervening call to operator delete[].

Section [expr.delete] (8.5.2.5)

  1. If the value of the operand of the delete-expression is not a null pointer value, the delete-expression will invoke the destructor (if any) for the object or the elements of the array being deleted. In the case of an array, the elements will be destroyed in order of decreasing address (that is, in reverse order of the completion of their constructor; see 15.6.2).

Section [basic.life] (6.6.3)

  1. A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. For an object of a class type with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a delete-expression (8.5.2.5) is not used to release the storage, the destructor shall not be implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.
  1. Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see 15.7. Otherwise, such a glvalue refers to allocated storage (6.6.4.4.1), and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if: [...] the glvalue is used to call a non-static member function of the object

So technically, since the destructor is a non-static member function of std::byte, and operator delete[] calls it (even though it does nothing) this is UB.

Edit 2:

As @NathanOliver pointed out, std::byte (Which is an enum class for char) does not have a destructor, so the above works fine.

If your storage class had a destructor, you would have to construct objects to then destruct. However, you can directly call the deallocation function void operator delete[](void*) so that no destructors are called. You should also replace std::make_unique<std::byte[]>(size) with:

std::unique_ptr<std::byte>{reinterpret_cast<std::byte*>(::operator new[](size)), [](std::byte* ptr) {     ::operator delete[](reinterpret_cast<void*>(ptr)); }} 

Because you can only operator delete[] stuff that has been operator new[]d (Which is not guaranteed by new[] expressions.)

At this point, you could also replace it with the non-array operator new.

T* ptr = new (memory.get()) T(); memory.release(); return std::shared_ptr<T>(ptr, [](T* ptr) {     ptr->~T();     ::operator delete[](reinterpret_cast<void*>(ptr)); }); 

The first version is still correct, and more reasonable. I would only use it if you are using a class with a destructor as a storage class (e.g. a union with non trivial types or a base class), you might have better luck finding out your compiler specific deallocate-array function.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: