I have a simple property<T>
class with a value_changed<T>
which you can connect
/disconnect
to and receive or inhibit events when value_changed<T>::emit(T)
is called. Think Qt signal/slots on C++11 steroids.
My next challenge is to provide a property-like object that is composed of sub-properties. Think about for example, a position, or size, which both consist of multiple values. I would like to be able to treat the subobjects as property
, and additionally get a composed signal emitted when multiple values are changed at once. E.g. doing
struct
{
property<int> x;
property<int> y;
}
position2d pos{0,0};
// ...
pos = {1,1}; // this should fire x.value_changed, y.value_changed, and pos.value_changed (once!)
This last little word is the core of the problem. I'm struggling to code a reusable composite_property
that can be customized with subobject names (position would get x
,y
, but size would get width
/height
).
Note a property<struct { int x; int y; }>
doesn't suffice: changing x
won't emit the composite value_changed
signal.
The best I can come up with is something with a bunch of boilerplate code to connect/disconnect the subobjects when assigning to the superobject, which is tedious and goes against the DRY principle.
I'm open to wild template magic, although I understand the free naming of the variables (x
/y
and width
/height
) will make at least some boilerplate code necessary.
EDIT For completeness, this is the property
template as I have it now:
template<typename T>
class property
{
public:
using value_type = T;
using reference = std::add_lvalue_reference_t<T>;
using const_reference = std::add_lvalue_reference_t<std::add_const_t<T>>;
using rvalue_reference = std::add_rvalue_reference_t<T>;
property(const_reference value_ = {}) : value(value_) {}
operator const_reference() const { return value; }
property& operator=(const_reference& other)
{
const bool changed = value != other;
value = other;
if(changed)
value_changed.emit(value);
return *this;
}
bool operator==(const_reference other) const { return value == other; }
bool operator!=(const_reference other) const { return value != other; }
bool operator< (const_reference other) const { return value < other; }
bool operator<=(const_reference other) const { return value <= other; }
bool operator> (const_reference other) const { return value > other; }
bool operator>=(const_reference other) const { return value >= other; }
signal<value_type> value_changed;
private:
value_type value;
};
signal
is a bit more involved, and is available here. Basically, connect
like Qt, except that it returns a connection_type
object like Boost.Signal, which can be used to disconnect
that connection.
Note I'm open to a backdoor "modify property silently" function that bypasses the signal, but that only implements half of what I need.
Since the question is tagged c++1z, here's a simple solution that's using some shiny new C++17 features (along the lines discussed in the comments above):
template<class T, auto... PMs> struct composite_property : property<T>
{
using const_reference = typename property<T>::const_reference;
composite_property(const_reference v = {}) : property<T>(v)
{
(... , (this->value.*PMs).value_changed.connect([this](auto&&)
{
if(listen_to_members) this->value_changed.emit(this->value);
}));
}
composite_property& operator=(const_reference& other)
{
listen_to_members = false;
property<T>::operator=(other);
listen_to_members = true; // not exception-safe, should use RAII to reset
return *this;
}
private:
bool listen_to_members = true;
};
Out of sheer laziness, I've made a change to your property<T>
: I've made value
public. Of course, there are several ways to avoid that, but they're unrelated to the problem at hand, so I opted to keep things simple.
We can test the solution using this toy example:
struct position2d
{
property<int> x;
property<int> y;
position2d& operator=(const position2d& other)
{
x = other.x.value;
y = other.y.value;
return *this;
}
};
bool operator!=(const position2d& lhs, const position2d& rhs) { return lhs.x.value != rhs.x.value || lhs.y.value != rhs.y.value; }
int main()
{
composite_property<position2d, &position2d::x, &position2d::y> pos = position2d{0, 0};
pos.value.x.value_changed.connect([](int x) { std::cout << " x value changed to " << x << '\n'; });
pos.value.y.value_changed.connect([](int y) { std::cout << " y value changed to " << y << '\n'; });
pos.value_changed.connect([](auto&& p) { std::cout << " pos value changed to {" << p.x << ", " << p.y << "}\n"; });
std::cout << "changing x\n";
pos.value.x = 7;
std::cout << "changing y\n";
pos.value.y = 3;
std::cout << "changing pos\n";
pos = {3, 7};
}
Here's a live example with all the necessary definitions included (my code is at the bottom).
While having to list the members explicitly as arguments to the composite_property
template can be annoying, it also provides quite a bit of flexibility. We can have, for example, a class with three member properties and define different composite properties over different pairs of member properties. The containing class is not affected by any of this and can also work independently of any composite properties, with the members used as standalone properties.
Note that there's a reason for the user-provided copy assignment operator of position2d
: if we left it defaulted, it would copy the member properties themselves, which doesn't emit signals, but rather duplicates the source properties.
The code works on Clang trunk in C++1z mode. It also causes an ICE on GCC trunk; we should try to reduce the example to get something that can be submitted in a bug report.
The crucial C++17 feature at work here is auto
non-type template parameters. In previous language versions, there are some much uglier alternatives, for example, wrapping the pointers to members in something like ptr_to_mem<decltype(&position2d::x), &position2d::x>
, possibly using a macro to avoid the repetition.
There's also a fold expression over ,
in the implementation of the constructor of composite_property
, but that can also be done (in a slightly more verbose way) by initializing a dummy array.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With