13

Beware, we're skirting the dragon's lair.

Consider the following two classes:

struct Base {
    std::string const *str;
};

struct Foo : Base {
    Foo() { std::cout << *str << "\n"; }
};

As you can see, I'm accessing an uninitialized pointer. Or am I?

Let's assume I'm only working with Base classes that are trivial, nothing more than (potentially nested) bags of pointers.

static_assert(std::is_trivial<Base>{}, "!");

I would like to construct Foo in three steps:

  1. Allocate raw storage for a Foo

  2. Initialize a suitably-placed Base subobject via placement-new

  3. Construct Foo via placement-new.

My implementation is as follows:

std::unique_ptr<Foo> makeFooWithBase(std::string const &str) {

    static_assert(std::is_trivial<Base>{}, "!");

    // (1)
    auto storage = std::make_unique<
        std::aligned_storage_t<sizeof(Foo), alignof(Foo)>
    >();

    Foo * const object = reinterpret_cast<Foo *>(storage.get());
    Base * const base = object;

    // (2)
    new (base) Base{&str};

    // (3)
    new (object) Foo(); 

    storage.release();
    return std::unique_ptr<Foo>{object};
}

Since Base is trivial, my understanding is that:

  • Skipping the trivial destructor of the Base constructed at (2) is fine;

  • The trivial default constructor of the Base subobject constructed as part of the Foo at (3) does nothing;

And so Foo receives an initialized pointer, and all is well.

Of course, this is what happens in practice, even at -O3 (see for yourself!).
But is it safe, or will the dragon snatch and eat me one day?

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • There is no guarantee that `(void*)(Base*)pDerived == (void*)pDerived`. – n. m. could be an AI Mar 22 '17 at 10:05
  • 2
    @n.m. I make no such assumption. That's exactly what the `Base * const base = object;` is for, adjusting via an implicit conversion. – Quentin Mar 22 '17 at 10:07
  • 1
    Oh I see. My bad, must look more better next time. But then who says you can convert a Foo* to a Base* if the pointer doesn't in fact point to a Foo object? – n. m. could be an AI Mar 22 '17 at 10:18
  • 1
    @n.m. No worries. This is in fact another concern -- do you reckon I should put some emphasis? – Quentin Mar 22 '17 at 10:24
  • This is guaranteed to break for virtual inheritance, and I don't remember the standard making any additional guarantees for non-virtual inheritance, so yes, there's a basis for being concerned. – n. m. could be an AI Mar 22 '17 at 10:27
  • [Hello, dragons](http://coliru.stacked-crooked.com/a/5bbebcfe7b59f17d). – T.C. Mar 22 '17 at 19:25
  • [@Columbo and I](http://chat.stackoverflow.com/rooms/136180/discussion-between-columbo-and-t-c) recently had a discussion about this. Briefly: the created object has dynamic storage duration, therefore the storage for it obtained from an allocation function has indeterminate value, and if no initialization is performed, it still has indeterminate value. The reserved non-allocating placement allocation function is still an allocation function, therefore the same rule apply regardless of any value that may have been previously written to it. – T.C. Mar 22 '17 at 19:45
  • @n.'pronouns'm. "_I don't remember the standard making any additional guarantees for non-virtual inheritance_" In practice, all base class subobject that don't have virtual bases are constructed as if they were complete objects (there is one constructor called in the both cases); OTOH those that have virtual bases have two ctors. There is no codification of that in the std. – curiousguy Dec 14 '19 at 03:18

1 Answers1

8

This seems to be explicitly disallowed by the standard. Ending an objects lifetime, and starting a new objects lifetime in the same location is explicitly allowed, unless it's a base class:

§3.8 Object Lifetime

§3.8.7 - If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and

  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and

  • [snip] and

  • the original object was a most derived object (1.8) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

sp2danny
  • 7,488
  • 3
  • 31
  • 53
  • Shoot. That's just cruel :( – Quentin Mar 22 '17 at 10:32
  • 2
    This paragraph only deals with when a reference/pointer/name to the original object can be reused to refer to the new object. I don't see any place in the OP's code that does that. – T.C. Mar 22 '17 at 19:28
  • 1
    In the latest draft, that clause which I call the "undead clause" was modified: there is no explicit mention of "most derived object" in the clause. But there is another, new requirement, and it rules out base subobjects. – curiousguy Dec 14 '19 at 03:16