Zero-cost properties with data member syntax

Here is what the common-initial-sequence rule says about unions:

In a standard-layout union with an active member of struct type T1, it is permitted to read a non-static data member m of another union member of struct type T2 provided m is part of the common initial sequence of T1 and T2; the behavior is as if the corresponding member of T1 were nominated.

Your code does not qualify. Why? Because you are not reading from "another union member". You are doing m.x = 42;. That isn't reading; that's calling a member function of another union member.

So it doesn't qualify for the common initial sequence rule. And without the common-initial-sequence rule to protect you, accessing non-active members of the union is UB.


TL;DR This is UB.

[basic.life]

Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a glvalue refers to allocated storage, and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if: [...]

  • the glvalue is used to call a non-static member function of the object, or

By definition, an inactive member of an union isn't within its lifetime.


A possible workaround is to use C++20 [[no_unique_address]]

struct Point
{
    int& get_x() { return xy[0]; }
    int& get_y() { return xy[1]; }
    [[no_unique_address]] property<Point, int, &Point::get_x> x;
    [[no_unique_address]] property<Point, int, &Point::get_y> y;
    std::array<int, 2> xy;
};

static_assert(offsetof(Point, x) == 0 && offsetof(Point, y) == 0);