4

Updated below: In clang, using an lvalue of a polymorphic object through its name does not activate virtual dispatch, but it does through its address.

For the following base class B and derived D, virtual function something, union Space

#include <iostream>
using namespace std;

struct B {
    void *address() { return this; }
    virtual ~B() { cout << "~B at " << address() << endl; }
    virtual void something() { cout << "B::something"; }
};

struct D: B {
    ~D() { cout << "~D at " << address() << endl; }
     void something() override { cout << "D::something"; }
};

union Space {
    B b;
    Space(): b() {}
    ~Space() { b.~B(); }
};

If you have a value s of Space, in Clang++: (update: incorrectly claimed g++ had the same behavior) If you do s.b.something(), B::something() will be called, not doing the dynamic binding on s.b, however, if you call (&s.b)->something() will do the dynamic binding to what b really contains (either a B or D). The completion code is this:

union SpaceV2 {
    B b;
    SpaceV2(): b() {}
    ~SpaceV2() { (&b)->~B(); }
};

static_assert(sizeof(D) == sizeof(B), "");
static_assert(alignof(D) == alignof(B), "");

#include <new>

int main(int argc, const char *argv[]) {
    {
        Space s;
        cout << "Destroying the old B: ";
        s.b.~B();
        new(&s.b) D;
        cout << "\"D::something\" expected, but \"";
        s.b.something();
        cout << "\" happened\n";
        auto &br = s.b;
        cout << "\"D::something\" expected, and \"";
        br.something();
        cout << "\" happened\n";
        cout << "Destruction of D expected:\n";
    }
    cout << "But did not happen!\n";
    SpaceV2 sv2;
    new(&sv2.b) D;
    cout << "Destruction of D expected again:\n";
    return 0;    
}

When compile with -O2 optimization and I run the program, this is the output:

$./a.out 
Destroying the old B: ~B at 0x7fff4f890628
"D::something" expected, but "B::something" happened
"D::something" expected, and "D::something" happened
Destruction of D expected:
~B at 0x7fff4f890628
But did not happen!
Destruction of D expected again:
~D at 0x7fff4f890608
~B at 0x7fff4f890608

What surprises me is that setting the dynamic type of s.b using placement new leads to a difference calling something on the very same l-value through its name or through its address. The first question is essential, but I have not been able to find an answer:

  1. Is doing placement new to a derived class, like new(&s.b) D undefined behavior according to the C++ standard?
  2. If it is not undefined behavior, is this choice of not activating virtual dispatch through the l-value of the named member something specified in the standard or a choice in G++, Clang?

Thanks, my first question in S.O. ever.

UPDATE The answer and the comment that refers to the standard are accurate: According to the standard, s.b will forever refer to an object of exact type B, the memory is allowed to change type, but then any use of that memory through s.b is "undefined behavior", that is, prohibited, or that the compiler can translate however it pleases. If Space was just a buffer of chars, it would be valid to in-place construct, destruct, change the type. Did exactly that in the code that led to this question and it works with standards-compliance AFAIK. Thanks.

TheCppZoo
  • 1,219
  • 7
  • 12
  • 1
    The placement `new` is fine. Using `s.b` to refer to the newly created object isn't. – T.C. Jan 04 '17 at 04:32
  • But surely there is no guarantee that `D` would be the same size as `B`? – wally Jan 04 '17 at 04:36
  • 2
    @Muscampester - there are `static_assert`s that guarantee the size and alignment are ok. – Mark Lakata Jan 04 '17 at 04:38
  • @t.c. thanks for the comment, do you have any leads to find the places in the standards where the setting (or changing) of the dynamic type and the differences of dispatch are specified? – TheCppZoo Jan 04 '17 at 04:39
  • 2
    [basic.life]/1, 6-8. `s.b` refers to an out-of-lifetime object, and both forms are undefined. (GCC devirtualizes the "working" version too at `-O3`.) – T.C. Jan 04 '17 at 04:43
  • @MarkLakata Ah ok, I missed that. Just went directly to debugging. But even so, the `B` class could be padded only for `D` to then have the padding somewhere else. I suppose we're just left with aliasing issues then. – wally Jan 04 '17 at 04:43

1 Answers1

2

The expression new(&s.b) D; re-uses the storage named s.b and formerly occupied by a B for for storage of a new D.

However you then write s.b.something(); . This causes undefined behaviour because s.b denotes a B but the actual object stored in that location is a D. See C++14 [basic.life]/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

    [...]

The last bullet point is not satisfied because the new type differs.

(There are other potential issues later in the code too but since undefined behaviour is caused here, they're moot; you'd need to have a major design change to avoid this problem).

M.M
  • 138,810
  • 21
  • 208
  • 365
  • Thanks for the answer. The solution is to use a raw buffer, I was attracted to the convenience of doing `thing.base`, but it is *UB*. The standard allows type punning through and from buffers of `char` or `unsigned char`, in the section you pointed to it is clear that you can placement-new and explicit-destructor-call as required – TheCppZoo Jan 04 '17 at 06:18
  • 1
    One can argue that "the new object" is the `B` subobject of the `D`, assuming typical layout. That argument's ruled out by bullet #4 in your [...]. – T.C. Jan 04 '17 at 07:05