Skip to content

88 Advanced The Hundred

One Object, Two Addresses

the button that is in two places at once

struct Drawable {
    virtual void draw() const { std::cout << "Drawable::draw\n"; }
    virtual ~Drawable() = default;
};
struct Clickable {
    virtual void click() const { std::cout << "Clickable::click\n"; }
    virtual ~Clickable() = default;
};
struct Button : Drawable, Clickable {};

int main() {
    Button button;
    Button* b = &button;
    Drawable* d = b;          // the same button,
    Clickable* c = b;         // viewed three ways
    const void* handle = c;   // park its address somewhere generic
    std::cout << std::boolalpha << "b == c ...... " << (b == c) << "\n";
    std::cout << "b == handle . " << (b == handle) << "\n";
    std::cout << "b " << static_cast<const void*>(b) << "  d " << static_cast<const void*>(d)
              << "  c " << static_cast<const void*>(c) << "\n";
}

Run it. Do the two checks agree?

Answer

They don't — and the last line shows why. Typical output (GCC 11 on x86-64):

b == c ...... true
b == handle . false
b 0x7ffc8e70e9a0  d 0x7ffc8e70e9a0  c 0x7ffc8e70e9a8

Why

A Button holds a Drawable and a Clickable back to back, and two non-empty subobjects cannot share an address, so at most one base can start where the Button does. Which one gets offset 0 is the ABI's call; here Clickable begins 8 bytes in, past Drawable's vtable pointer. b == c is still true only because comparing related pointers converts one operand first — but a void* has no class to adjust for, so handle keeps the Clickable address.

The fix

const void* handle = dynamic_cast<const void*>(c);   // most-derived address: now == b
static_cast<Clickable*>(b)->click();                 // adds the 8: prints Clickable::click

The trap is that same call with reinterpret_cast<Clickable*>(b): no adjustment, so the call lands on the Button address and is undefined behavior. That is the line -DSHOW_BUG adds, and it still compiles — click() runs through Drawable's vtable and prints Drawable::draw (GCC 11 at -O0/-O1, Clang 18 everywhere), or segfaults and drops a core file (GCC 11 at -O2/-O3). -Wall -Wextra says nothing; Clang warns by default: -Wreinterpret-base-class, use 'static_cast' to adjust the pointer correctly while upcasting.

Takeaway: a pointer's numeric value depends on the type you view the object through.

Try it: g++ -std=c++17 -Wall -Wextra main.cpp -o demo && ./demo; -DSHOW_BUG adds the trap.

Open in Compiler Explorer ↗ Open SHOW_BUG variant ↗ Quiz this entry