Specification of P0137 Features

In the following code, I was meticulous in the following standard words (plus in light of wording P0137 ) for the lifetime of the object.

Please note that all memory allocation is done using a suitable location of type unsigned char, as in P0137.

Note also that Foo is a POD with a trivial constructor.

Questions:

but. If I misunderstood the standard, and there is UB, please kindly specify it (or, alternatively, make sure there is no UB)

B. Are initializations in A, B, C, D, E, F strictly necessary in light of the fact that the construction is trivial and does not perform any actual initialization. If yes, indicate which part of the standard contradicts or clarifies [object.lifetime] in this regard.

the code:

 #include <memory> // a POD with trivial constructor struct Foo { int x; }; struct destroy1 { void operator()(Foo* p) { // RAII to guarantee correct destruction order auto memory = std::unique_ptr<unsigned char[]>(reinterpret_cast<unsigned char*>(p)); p->~Foo(); // A } }; std::unique_ptr<Foo, destroy1> create1() { // RAII to guarantee correct exception handling auto p = std::make_unique<unsigned char[]>(sizeof(Foo)); auto pCandidate = reinterpret_cast<Foo*>(p.get()); new (pCandidate) Foo(); // B return std::unique_ptr<Foo, destroy1>(reinterpret_cast<Foo*>(p.release()), destroy1()); } struct call_free { void operator()(void *p) const { std::free(p); } }; using malloc_ptr = std::unique_ptr<unsigned char, call_free>; struct destroy2 { void operator()(Foo *pfoo) const { // RAII to guarantee correct destruction order auto memory = malloc_ptr(reinterpret_cast<unsigned char*>(pfoo)); pfoo->~Foo(); // C } }; std::unique_ptr<Foo, destroy2> create2() { // RAII to guarantee correct exception handling auto p = malloc_ptr(reinterpret_cast<unsigned char*>(std::malloc(sizeof(Foo)))); auto pCandidate = reinterpret_cast<Foo*>(p.get()); new (pCandidate) Foo(); // D return std::unique_ptr<Foo, destroy2>(reinterpret_cast<Foo*>(p.release()), destroy2()); } struct nodelete { void operator()(Foo * p) { p->~Foo(); // E } }; std::shared_ptr<Foo> provide() { alignas(Foo) static unsigned char storage[sizeof(Foo)]; auto make = [] { auto p = reinterpret_cast<Foo*>(storage); new (p) Foo (); // F return std::shared_ptr<Foo>(p, nodelete()); }; static std::shared_ptr<Foo> pCandidate = make(); return pCandidate; } int main() { auto foo1 = create1(); auto foo2 = create2(); auto foo3 = provide(); foo1->x = 1; foo2->x = 2; foo3->x = 3; } 
+10
c ++ language-lawyer c ++ 17
Dec 02 '16 at 10:52
source share
2 answers

On create1

 std::unique_ptr<Foo, destroy1>(reinterpret_cast<Foo*>(p.release()), destroy1()); 

This does not work because you are using the wrong pointer.

p.release() thinks it points to an unsigned char[] . However, this is not the object you want to point to. What you want to specify is the object that lives inside this array, Foo , which you created.

So now you obey [basic.life] / 8. The bottom line is that you can use the previous pointer only as a pointer to a new object, if they are of the same type. That they are not in your case.

Now I can tell you the launder pointer, but a more reasonable way to handle this is to simply save the pointer returned by the new call:

 auto p = std::make_unique<unsigned char[]>(sizeof(Foo)); auto ret = std::unique_ptr<Foo, destroy1>(new(p.get()) Foo(), destroy1()); p.release(); return ret; 

This pointer will always be correct.

Your use of the new placement is optional. [intro.object] / 1 tells us:

An object is created by definition (3.1), a new expression (5.3.4), when an active member of an association (9.3) is implicitly changed, or when a temporary object is created (4.4, 12.2).

When you allocate unsigned char[] , then the object that you created in this repository. You can't just pretend it's Foo , simply because Foo is an aggregate. [intro.object] / 1 does not allow this. You must explicitly create this object using one of the mechanisms listed above. Since you cannot use a definition, union member activation, or temporary objects with arbitrary memory buffers to create objects from an existing repository, the only way you must create is a new expression.

In particular, the placement is new.

As for delete1 , you will need a custom div, since default will cause delete on the Foo pointer to default. Your code is as follows:

 auto memory = std::unique_ptr<unsigned char[]>(reinterpret_cast<unsigned char*>(p)); p->~Foo(); 

unsigned char[] has some special logic for it, in terms of how it behaves when placing objects in their storage, thanks to [intro.object] / 3-4. If the object completely overlaps the unsigned char[] storage, it functions as if the object was allocated inside the array. This means that unsigned char[] still technically exists; it does not destroy the byte array.

So you can still remove the byte array that your code does here.

On create2

This is also incorrect due to further violations of [basic.life] / 8. The fixed version will be similar to the one above:

 auto p = malloc_ptr(reinterpret_cast<unsigned char*>(std::malloc(sizeof(Foo)))); auto ret std::unique_ptr<Foo, destroy2>(new(p.get()) Foo(), destroy2()); p.release(); return ret; 

Unlike new expressions, malloc never creates an object through [intro.object] / 1; he only acquires storage. Thus, accommodation is again required - new.

Similarly, free just frees up memory; this does not apply to objects. Thus, your delete2 is essentially perfect (although using malloc_ptr makes it useless confusing).

On provide

This object has the same [basic.life] / 8 problems as the rest of your examples:

 alignas(Foo) static unsigned char storage[sizeof(Foo)]; static auto pCandidate = std::shared_ptr<Foo>(new(storage) Foo(), nodelete()); return pCandidate; 

But other than that, it's fine (until you break it elsewhere). What for? This complex.

[basic.start.term] / 1 tells us that static objects are destroyed in the reverse order of their initialization. And [stmt.decl] / 4 tells us that static objects with a block scope are initialized in the order in which they appear in the function.

Therefore, we know that pCandidate will be destroyed before storage . Until you save a copy of this shared_ptr in a static variable or otherwise fail to destroy / reset all such shared objects before completion, you should be fine.




That being said, using unsigned char blocks is really pre-C ++ 11. Now we are std::aligned_storage and std::aligned_union . Use them.

+6
Dec 02 '16 at 18:10
source share

โ€œCore Question 1776: Replacing Class Objects Containing Reference Membersโ€ is based on an obvious and serious interpretation error, and as such should be rejected. The error is here:

Note: this confirms the status quo that one malloc is not enough to create an object

This contradicts the status quo that "malloc alone" is really enough to create an object, since malloc returns the appropriate storage alignment, which has always been and was sufficient to create an object.

The underlying problem is not standard. This is an opinion about the standard. This opinion is erroneous.

-one
Jan 30 '17 at 1:54 on
source share



All Articles