Zero-cost properties with data member syntax

  • A+
Category:Languages

I have (re?)invented this approach to zero-cost properties with data member syntax. By this I mean that the user can write:

some_struct.some_member = var; var = some_struct.some_member; 

and these member accesses redirect to member functions with zero overhead.

While initial tests show that the approach does work in practice, I'm far from sure that it is free from undefined behaviour. Here's the simplified code that illustrates the approach:

template <class Owner, class Type, Type& (Owner::*accessor)()> struct property {     operator Type&() {         Owner* optr = reinterpret_cast<Owner*>(this);         return (optr->*accessor)();     }     Type& operator= (const Type& t) {         Owner* optr = reinterpret_cast<Owner*>(this);         return (optr->*accessor)() = t;     } };  union Point {     int& get_x() { return xy[0]; }     int& get_y() { return xy[1]; }     std::array<int, 2> xy;     property<Point, int, &Point::get_x> x;     property<Point, int, &Point::get_y> y; }; 

The test driver demonstrates that the approach works and it is indeed zero-cost (properties occupy no additional memory):

int main() {     Point m;     m.x = 42;     m.y = -1;      std::cout << m.xy[0] << " " << m.xy[1] << "/n";     std::cout << sizeof(m) << " " << sizeof(m.x) << "/n"; } 

Real code is a bit more complicated but the gist of the approach is here. It is based on using a union of real data (xy in this example) and empty property objects. (Real data must be a standard layout class for this to work).

The union is needed because otherwise properties needlessly occupy memory, despite being empty.

Why do I think there's no UB here? The standard permits accessing the common initial sequence of standard-layout union members. Here, the common initial sequence is empty. Data members of x and y are not accessed at all, as there are no data members. My reading of the standard indicate that this is allowed. reinterpret_cast should be OK because we are casting a union member to its containing union, and these are pointer-interconvertible.

Is this indeed allowed by the standard, or I'm missing some UB here?

 


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

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: