Some time ago, we looked at making the default parameter of a method be the this
pointer of the caller. The scenario was something like this:
struct Property { Property(char const* name, int initial, Object* owner) : m_name(name), m_value(initial), m_owner(owner) {} ⟦ other methods elided - use your imagination ⟧ char const* m_name; Object* m_owner; int m_value; }; struct Widget : Object { Property Height{ "Height", 10, this }; Property Width{ "Width", 10, this }; };
and we didn’t want to have to type this
as the last parameter to all the Property
constructors. We came up with this:
template<typename D> struct PropertyHelper { Property Prop(char const* name, int initial) { return Property(name, initial, static_cast<D*>(this)); } }; struct Widget : Object, PropertyHelper<Widget> { Property Height = Prop("Height", 10); Property Width = Prop("Width", 10); };
Or, if you have access to deducing this,
struct PropertyHelper { template<typename Parent> Property Prop(this Parent&& parent, char const* name, int initial) { return Property(name, initial, &parent); } }; struct Widget : Object, PropertyHelper { Property Height = Prop("Height", 10); Property Width = Prop("Width", 10); };
But this uses a fixed parameter list. What if you have to deal with multiple kinds of properties?
template<typename T> struct Property { Property(char const* name, T const& initial, Object* owner) : m_name(name), m_value(initial), m_owner(owner) {} ⟦ other methods elided - use your imagination ⟧ char const* m_name; Object* m_owner; T m_value; };
or arbitrary types, not just specializations of Property
?
template<typename Handler> struct Event { Event(char const* name, Object* owner) : m_name(name), m_owner(owner) {} ⟦ other methods elided - use your imagination ⟧ char const* m_name; Object* m_owner; std::vector<Handler> m_handlers; };
What all of these little classes have in common is that they take a pointer to the containing class as the final parameter. Can we generalize this solution without having to create a bunch of one-off classes, one for each type we need to wrap?
Sure. The idea is to use a trick we saw a little while ago: You can simulate a function that returns different types depending on what the caller wants by returning a proxy type that holds onto the parameters and then performs the work when the destination type is revealed by the conversion operator.
template<typename...Args> struct maker { std::tuple<Args&&...> m_args; maker(Args&&... args) : m_args((Args&&)args...) {} template<typename T> operator T() { return std::make_from_tuple<T>(std::move(m_args)); } }; template<typename D> struct OwnerHelper { template<typename...Args> auto Owned(Args&&... args) { return maker<Args&&..., D*>((Args&&)args..., static_cast<D*>(this)); } }; struct Widget : Object, OwnerHelper<Widget> { Property<int> Height = Owned("Height", 10); Property<std::string> Name = Owned("Name", ""s); Event<NameChangedHandler> NameChanged = Owned("NameChanged"); };
There is a subtlety here: We need to cast args
to ensure that its reference category is preserved.
Note that the use of a parameter pack means that we have to write something like ""s
or std::string{}
to get an empty string, rather than the more convenient {}
because template type parameters match types, and not braced things that could be used to construct a type but aren’t a type themselves.
As before, we can simplify this with deducing this:
struct OwnerHelper { template<typename O, typename...Args> auto Owned(this O&& self, Args&&... args) { return maker<Args&&..., O*>((Args&&)args..., &self); } }; struct Widget : Object, OwnerHelper { Property<int> Height = Owned("Height", 10); Property<std::string> Name = Owned("Name", ""s); Event<NameChangedHandler> NameChanged = Owned("NameChanged"); };
0 comments