Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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

like image 492
YSC Avatar asked Jan 08 '19 15:01

YSC


People also ask

Are smart pointers memory safe?

In modern C++ programming, the Standard Library includes smart pointers, which are used to help ensure that programs are free of memory and resource leaks and are exception-safe.

What problem does using smart pointers help prevent?

Smart pointers try to prevent memory leaks by making the resource deallocation automatic: when the pointer to an object (or the last in a series of pointers) is destroyed, for example because it goes out of scope, the pointed object is destroyed too.

Can smart pointers leak memory?

Even when using smart pointers, is it still possible to have memory leak ? Yes, if you are not careful to avoid creating a cycle in your references.

What is smart pointer when should we use it?

A Smart Pointer is a wrapper class over a pointer with an operator like * and -> overloaded. The objects of the smart pointer class look like normal pointers. But, unlike Normal Pointers it can deallocate and free destroyed object memory.


1 Answers

This program is not well defined.

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

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

is almost correct. 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 when the shared_ptr is ready to delete, it calls delete this->get();, which is wrong. That first deconstructs a T, but then it deallocates a T instead of a std::byte[], which will probably (undefined) not work.

C++ standard §8.5.2.4p8 [expr.new]

A new-expression may obtain storage for the object by calling an allocation function. [...] If the allocated type is an array type, the allocation function's name is operator new[].

(All those "may"s are because implementations are allowed to merge adjacent new expressions and only call operator new[] for one of them, but this isn't the case as new only happens once (In make_unique))

And part 11 of the same section:

When a new-expression calls an allocation function and that allocation has not been extended, the new-expression passes the amount of space requested to the allocation function as the first argument of type std::size_t. That argument shall be no less than the size of the object being created; it may be greater than the size of the object being created only if the object is an array. For arrays of char, unsigned char, and std::byte, the difference between the result of the new-expression and the address returned by the allocation function shall be an integral multiple of the strictest fundamental alignment requirement (6.6.5) of any object type whose size is no greater than the size of the array being created. [Note: Because allocation functions are assumed to return pointers to storage that is appropriately aligned for objects of any type with fundamental alignment, this constraint on array allocation overhead permits the common idiom of allocating character arrays into which objects of other types will later be placed. — end note ]

If you read §21.6.2 [new.delete.array], you see that the default operator new[] and operator delete[] do the exact same things as operator new and operator delete, the problem is we don't know the size passed to it, and it is probably more than what delete ((T*) object) calls (to store the size).

Looking at what delete-expressions do:

§8.5.2.5p8 [expr.delete]

[...] delete-expression will invoke the destructor (if any) for [...] the elements of the array being deleted

p7.1

If the allocation call for the new-expression for the object to be deleted was not omitted [...], the delete-expression shall call a deallocation function (6.6.4.4.2). The value returned from the allocation call of the new-expression shall be passed as the first argument to the deallocation function.

Since std::byte does not have a destructor, we can safely call delete[], as it will not do anything other than call the deallocate function (operator delete[]). We just have to reinterpret it back to std::byte*, and we will get back what new[] returned.

Another problem is that there is a memory leak if the constructor of T throws. A simple fix is to placement new while the memory is still owned by the std::unique_ptr, so even if it does throw it will call delete[] properly.

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

The first placement new ends the lifetime of the sizeof(T) std::bytes and starts the lifetime of a new T object at the same address, as according to §6.6.3p5 [basic.life]

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. [...]

Then when it is being deleted, the lifetime of T ends by an explicit call of the destructor, and then according to the above, the delete-expression deallocates the storage.


This leads to the question of:

What if the storage class wasn't std::byte, and was not trivially destructible? Like, for example, we were using a non-trivial union as the storage.

Calling delete[] reinterpret_cast<T*>(ptr) would call the destructor on something that is not an object. This is clearly undefined behaviour, and is according to §6.6.3p6 [basic.life]

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated [...], any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. [...] The program has undefined behavior if: the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a delete-expression

So to use it like above, we have to construct it just to destruct it again.

The default constructor probably works fine. The usual semantics are "create an object that can be destructed", which is exactly what we want. Use std::uninitialized_default_construct_n to construct them all to then immediately destruct them:

    // Assuming we called `new StorageClass[n]` to allocate     ptr->~T();     auto* as_storage = reinterpret_cast<StorageClass*>(ptr);     std::uninitialized_default_construct_n(as_storage, n);     delete[] as_storage; 

We can also call operator new and operator delete ourselves:

static void byte_deleter(std::byte* ptr) {     return ::operator delete(reinterpret_cast<void*>(ptr)); }  auto non_zero_memory(std::size_t size) {     constexpr std::byte non_zero = static_cast<std::byte>(0xC5);      auto memory = std::unique_ptr<std::byte, void(*)(std::byte*)>(         reinterpret_cast<std::byte*>(::operator new(size)),         &::byte_deleter     );     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));     T* ptr = new (memory.get()) T();     memory.release();     return std::shared_ptr<T>(ptr, [](T* ptr) {         ptr->~T();         ::operator delete(ptr, sizeof(T));                             // ^~~~~~~~~ optional     }); } 

But this looks a lot like std::malloc and std::free.

A third solution might be to use std::aligned_storage as the type given to new, and have the deleter work as with std::byte because the aligned storage is a trivial aggregate.

like image 127
Artyer Avatar answered Oct 27 '22 14:10

Artyer