Difference calling virtual through named member versus address or reference

114 views Asked by At

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.

1

There are 1 answers

2
M.M On BEST ANSWER

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).