A Proper Property
A continuation of Effective Implementation of C++ Properties
by Viktor Korsun
Gašper Ažman
Recap:
What's a property?
It pretends it's a field
x.foo = 5;
Assignment
int bar = x.foo;
Reading
If it quacks like a field, it's gotta be...
struct Airplane {
int cargo;
} airplane;
... a field, right?
Except it's more like a getter and a setter.
Storage is optional
struct Airplane {
struct Cargo {
int operator=(int y) {
/* haha, ignore */
return 0;
}
operator int() const {
return 0;
}
};
Cargo cargo;
} airplane;
Perfectly valid opinions
Real programmers don't use properties
It is bad design; methods do exist.
That's that Java thing, right?
I can fix my codebase with sed!
But what if I need to change a widely used bad interface?*
* or another of a million reasons they are handy, sometimes
You need properties when you want to interact with the host object
Otherwise, just use a type.
Try 1: KISS
struct Airplane {
bool has_cargo;
struct Cargo {
int operator=(int y) {
has_cargo = rand()%2;
return 0;
}
operator int() const {
return has_cargo ? rand()%5 : 0;
}
};
Cargo cargo;
} airplane;
Problem: The above does not work.
Methods of Cargo need
(Airplane*) this
not (Cargo*) this
Try 2: KISS
Save the this pointer
struct Airplane {
bool has_cargo;
struct Cargo {
int operator=(int y) {
host->has_cargo = rand()%2;
return 0;
}
operator int() const {
return host->has_cargo ? rand()%5 : 0;
}
Airplane* host;
};
Cargo cargo;
// constructor
Airplane() : cargo{this} {}
} airplane;
OK, THIS WORKS
But sizeof(Airplane) is now 16 bytes!
Way bigger than just that bool!
Try 3: Three sheets to the wind
We could put the offset in the type of the property!
Object layout
Airplane |
---|
bool is_cargo; |
Cargo cargo; |
Fixed offset
(Airplane*) = (Cargo*) this - offset
... with a sprinkle of reinterpret casts to char*
Getting the offset
struct Airplane {
bool has_cargo;
struct Cargo {
// ...
};
Cargo cargo;
} airplane;
static auto const cargo_offset = offsetof(Airplane, cargo);
Using the offset
Airplane* cargo_to_airplane(Cargo* cargo) {
auto const raw_cargo = reinterpret_cast<char*>(cargo);
auto const raw_airplane = raw_cargo - cargo_offset;
return reinterpret_cast<Airplane*>(raw_airplane);
}
struct Airplane {
bool has_cargo;
struct Cargo {
// ...
};
Cargo cargo;
} airplane;
static auto const cargo_offset = offsetof(Airplane, cargo);
... And, that's it. We can do this:
static auto const cargo_offset;
struct Airplane {
bool has_cargo;
struct Cargo {
operator=(int x) {
cargo_to_airplane(this)->has_cargo = rand()%x;
}
};
Cargo cargo;
} airplane;
static auto const cargo_offset = offsetof(Airplane, cargo);
Damn that's ugly.
All sorts of things are wrong with it. We want this:
struct Airplane {
bool has_cargo;
auto set_cargo(int x) {
return has_cargo = rand()%x;
}
auto get_cargo() const {
return has_cargo;
}
PROPERTY(cargo, get_cargo, set_cargo);
};
To do that, we need contiguous code.
But offsetof() does *not* work inside the class. It needs a complete type.
Dealing with it:
struct Airplane {
bool has_cargo;
struct Cargo {
operator=(int x) {
cargo_to_airplane(this)->has_cargo = rand()%x;
}
};
Cargo cargo;
// We can't call offsetof, but we *can* do this.
static auto constexpr cargo_address() const {
return &Airplane::cargo;
}
} airplane;
Dealing with it: member pointer to offset
template <typename Host, typename Member>
static auto constexpr offset_of(Member* cargo) {
return reinterpret_cast<size_t>(&(Host*)0->*cargo));
}
Airplane* cargo_to_airplane(Cargo* cargo) {
auto const raw_cargo = reinterpret_cast<char*>(cargo);
auto const cargo_offset =
offset_of<Airplane>(Airplane::cargo_offset());
auto const raw_airplane = raw_cargo - cargo_offset;
return reinterpret_cast<Airplane*>(raw_airplane);
}
These are free functions, using a method in a type-dependent context
Ok, that's cool.
Contiguous code, not too much badness...
Let's do MORE CARGO
struct Airplane {
bool has_cargo;
struct Cargo {
operator=(int x) {
cargo_to_airplane(this)->has_cargo = rand()%x;
}
};
Cargo cargo;
Cargo more_cargo;
static auto constexpr cargo_address() const {
return &Airplane::cargo;
}
// ... THIS IS HORRIBLE.
static auto constexpr more_cargo_address() const {
return &Airplane::cargo;
}
} airplane;
Does not work.
There's a copy-paste bug on this slide. ->macro
Type tags to the rescue
template <typename T> type_ { using type = T; };
struct Airplane {
bool has_cargo;
template <typename Tag>
struct Cargo {
using tag = Tag;
operator=(int x) {
cargo_to_airplane<tag>(this)->has_cargo = rand()%x;
}
};
struct cargo_tag;
Cargo<cargo_tag> cargo;
static auto constexpr member_address(type_<cargo_tag>) const {
return &Airplane::cargo;
}
struct more_cargo_tag
Cargo<more_cargo_tag> more_cargo;
static auto constexpr member_address(type_<more_cargo_tag>) const {
return &Airplane::more_cargo;
}
} airplane;
The other side
template <typename Cargo>
Airplane* cargo_to_airplane(Cargo* cargo) {
auto const raw_cargo = reinterpret_cast<char*>(cargo);
auto const tag = type_<typename Cargo::tag>{};
auto const cargo_offset =
offset_of<Airplane>(Airplane::member_offset(tag)));
auto const raw_airplane = raw_cargo - cargo_offset;
return reinterpret_cast<Airplane*>(raw_airplane);
}
template <typename Host, typename Member>
static auto constexpr offset_of(Member* cargo) {
return reinterpret_cast<size_t>(&(Host*)0->*cargo));
}
And that's it!
... well, for the talk.
Talk to me later about how to
* make copying safe
* get rid of the one byte of overhead,
* make this a single-line macro.
Library's here:
https://github.com/bitekas/properties
A proper property
By Gašper Ažman
A proper property
Slide deck describing how to build a no-overhead class that can serve as an emulation of object properties (fields with getters and setters) in other languages.
- 2,041