You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two days ago, I watched a very interesting talk by Zach Laine: Pragmatic Type Erasure: Solving OOP Problems with an Elegant Design Pattern. The question that Zach Laine addresses is how to provide polymorphic interfaces while, at the same time, adhering to value semantics. I do also recommend the following talk by Sean Parent, which gives a great introduction into the concept and the benefits of type-erasure, and value semantics: Inheritance Is The Base Class of Evil
This post is motivated by a question that came up on Reddit. Namely, how can we merge multiple type-erased interfaces into one single interface. A similar question is also asked in the end of the first talk: How to apply type erasure to types with overlapping interfaces? The speaker’s answer is to simply repeat the common parts. I think there has to be a better way. So, in this post I am going to explore how to merge type-erased interfaces. But first, let’s quickly revise type-erasure.
(Note, the C++ code examples are simplified in favour of readability. A link to working code is provided at the end of each section.)
Type Erasure
Suppose we want to write a function which greets a person named Tom in some way. I.e. could print “Hi Tom”, “Hello Tom”, “Good day Tom”, … you get the idea. The function should accept an argument that specifies how to greet a person. We will call this argument a Greeter. Here is a simple implementation of our function:
voidgreet_tom(constGreeter&g){g.greet("Tom");}
A user of this function may now wish to greet Tom in English and in French. So, he implements two Greeters:
structEnglish{voidgreet(conststd::string&name)const{std::cout<<"Good day "<<name<<". How are you?\n";}};
structFrench{ voidgreet(conststd::string&name)const{ std::cout<<"Bonjour "<<name<<". Comment ca va?\n"; } };
Now, how can the user pass his Greeters to our function? Classically, we could either define an abstract base class and let our user derive from it, or we could make greet_tom a function template in Greeter. Both methods have their down-sides, which are described in the above mentioned talks.
With type-erasure, we will hide the templates, and the inheritance under the covers. We will define a Greeter class that can be initialized with anything that provides the expected Greeter interface. Following Sean Parent’s pattern an implementation could look as follows:
classGreeter{public:// Constructor: We can stuff anything into a Greeter costume.template<classT>Greeter(Tdata):self_(std::make_shared<Model<T>>(data)){}
<span>// External interface: Just forward the call to the wrapped object.</span>
<span>void</span> <span>greet</span><span>(</span><span>const</span> <span>std</span><span>::</span><span>string</span> <span>&</span><span>name</span><span>)</span> <span>const</span> <span>{</span>
<span>self_</span><span>-></span><span>greet</span><span>(</span><span>name</span><span>);</span>
<span>}</span>
private: // The abstract base class is hidden under the covers... structConcept{ virtual~Concept()=default; virtualvoidgreet(conststd::string&)const=0; }; // ... and so are the templates. template<classT> classModel:publicConcept{ public: Model(Tdata):data_(data){} virtualvoidgreet(conststd::string&name)constoverride{ // Forward call to user type. // Requires that T can greet. data_.greet(name); }
<span>private</span><span>:</span>
<span>// The user defined Greeter will be stored here. (by value!)</span>
<span>T</span> <span>data_</span><span>;</span>
<span>};</span>
<span>// Polymorphic types require dynamic storage.</span>
<span>// Here we store our pointer to the Model that holds the users Greeter.</span>
<span>std</span><span>::</span><span>shared_ptr</span><span><</span><span>const</span> <span>Concept</span><span>></span> <span>self_</span><span>;</span>
};
Note that we are using a shared-pointer to const to refer to the implementation. The details are explained in Sean Parent’s talk. We get copy-on-write and value-semantics out of it for free (Magic!). I chose it here, because it eliminates all the boiler-plate for copy/move construction/assignment.
A working example of the code is available here.
Multiple Concepts
The problem arises when we want to merge two existing interfaces. For example, suppose there is a second concept: A door-opener, short Opener. I.e. a thing that opens doors. In some places of our code an Opener will be sufficient, in some other places we only need a Greeter, but in some places we need to first open the door for someone and then greet them:
But this is not ideal. It would be much better if we could take an existing Greeter concept, and an existing Opener concept, and just merge the two together.
A working example of the code is available here.
Dissecting Type-Erasure
Before we get there we need to understand what our type-erasure class actually does. So let’s take the Greeter apart.
First, it defines an abstract base class Concept. This is very specific to the Greeter. But, it has nothing to do with type-erasure. So, we pull it out.
// Defines the concept of a Greeter.structConcept{virtual~Concept()=default;virtualvoidgreet(conststd::string&name)const=0;};
Second, there is the model of that concept. This actually does two things: It holds an arbitrary value, and it passes the concept’s interface through to that value. So, let’s separate them.
// Holds a value of arbitrary type.template<classT>classHolder{public:Holder(Tobj):data_(std::move(obj)){}virtual~Holder()=default;constT&get()const{returndata_;}
private: Tdata_; };
// Passes the Concept's interface through to the held value. template<classHolder> structModel:publicHolder,publicConcept{ usingHolder::Holder;// pull in holder's constructor virtualvoidgreet(conststd::string&name)constoverride{ this->Holder::get().greet(name); } };
Next, Greeter is also a container that refers to a concept, and initializes it with a model. This is very specific to type-erasure, but has nothing to do with greeting people.
And after all this hacking and slashing there is only one bit left. Namely, the external interface that passes calls through to the container.
template<classContainer>structExternalInterface:publicContainer{usingContainer::Container;// pull in container's constructorvoidgreet(conststd::string&name)const{this->Container::get().greet(name);}};
Great! We started out with a perfectly well functioning class and took it apart into tiny pieces. Now we need to reassemble them and make sure that it still works. But don’t forget, the goal of this exercise is to make concepts mergeable — automatically. Hence, we need an automated way to assemble all the pieces that we created. So, it’s time for some template magic.
Automated Type-Erasure
Above pieces fall into two categories: One, there are pieces that define the Greeter’s interface, and two, there are pieces which define how to hold and call objects of arbitrary types. On the holding and calling side we find Holder, and Container, which are implementation details of our type-erasure container; whereas Concept, Model, and ExternalInterface are details of a Greeter. To keep things in order we will collect the Greeter parts in a super type that we call GreeterSpec.
At this stage we can write a template class that assembles all these pieces together and constructs a type-erasure container for an arbitrary spec. It will take the spec’s ExternalInterface template, and instantiate it with a container for the spec’s concept, and model. It will also pull in the base-classes constructor, so that we can still construct it from objects of arbitrary types.
As the last line demonstrates, the Greeter itself is nothing but a TypeErasure of a certain spec.
Again, a working example of the code is available here.
Merging Concepts
Now, with all that machinery backing us, we can tackle the original problem: How to merge two concepts? We have a tool that creates a type-erasure class out of an arbitrary spec. And, we assume that we already have a GreeterSpec, and an OpenerSpec that define those two concepts. What we need is a tool to automatically merge two specs into one. Let’s approach this component by component.
How do we merge Concept classes, i.e. interfaces? In C++ we do this by multiple inheritance:
How about the Models? The model is a template class that takes a holder as a template parameter and then inherits from said holder, thus becoming a holder itself. So, we can take SpecB, and the holder, and merge them into one class. This new class will itself be a holder. Next, we take SpecA, and that new holder, and merge them to get our final merged Model. There is one nifty detail, though: We need to use virtual inheritance for the concepts. The reason is that ConceptA, and ConceptB will enter the merged Model through the merged Concept, but also through the models of the two specs.
And with just a little bit more of template magic it is even possible to merge two existing type-erasure classes. So, with all the above we write the following code:
We find that it is indeed possible to merge two existing type-erasure classes into one that has a common interface. And what’s more, we can do it fully automatically and in just one line of code. The costly bit is to define the original type-erasure classes. For each one we need to define a spec class, and manually define the interface. The reason is that C++ does not support introspection. On the other hand, these specs follow a fairly strict scheme and it should be quite possible to produce them through tooling, or possibly even a macro.
Another possible issue is the inheritance pattern for Model, and ExternalInterface. Due to the chaining of base classes we introduce user specified names into the classes Holder, and Container. The method get could be shadowed by a user method. The library code does actually contain more template magic to avoid this problem. A template meta-function peels layers of derived classes off until it arrives at the actual Holder, or Container class. An external getter function is provided for the user, which makes sure to call the correct getter method. An obscured name of the internal getter method provides further protection.
The full code is available here. Please feel invited to try it out and give me your feedback. Also, since this is my first blog post, any criticism is very welcome.
Thanks for reading!
Unfortunately, I have not yet figured out how to add comments to github pages. For the moment I would like to defer any discussion to Reddit. I apologize for the inconvenience.
via Andreas Herrmann
September 12, 2024 at 09:01AM
The text was updated successfully, but these errors were encountered:
Type Erasure with Merged Concepts
https://ift.tt/OYR4muK
Andreas Herrmann
Two days ago, I watched a very interesting talk by Zach Laine: Pragmatic Type Erasure: Solving OOP Problems with an Elegant Design Pattern. The question that Zach Laine addresses is how to provide polymorphic interfaces while, at the same time, adhering to value semantics. I do also recommend the following talk by Sean Parent, which gives a great introduction into the concept and the benefits of type-erasure, and value semantics: Inheritance Is The Base Class of Evil
This post is motivated by a question that came up on Reddit. Namely, how can we merge multiple type-erased interfaces into one single interface. A similar question is also asked in the end of the first talk: How to apply type erasure to types with overlapping interfaces? The speaker’s answer is to simply repeat the common parts. I think there has to be a better way. So, in this post I am going to explore how to merge type-erased interfaces. But first, let’s quickly revise type-erasure.
(Note, the C++ code examples are simplified in favour of readability. A link to working code is provided at the end of each section.)
Type Erasure
Suppose we want to write a function which greets a person named Tom in some way. I.e. could print “Hi Tom”, “Hello Tom”, “Good day Tom”, … you get the idea. The function should accept an argument that specifies how to greet a person. We will call this argument a Greeter. Here is a simple implementation of our function:
A user of this function may now wish to greet Tom in English and in French. So, he implements two Greeters:
Now, how can the user pass his Greeters to our function? Classically, we could either define an abstract base class and let our user derive from it, or we could make
greet_tom
a function template inGreeter
. Both methods have their down-sides, which are described in the above mentioned talks.With type-erasure, we will hide the templates, and the inheritance under the covers. We will define a
Greeter
class that can be initialized with anything that provides the expected Greeter interface. Following Sean Parent’s pattern an implementation could look as follows:Note that we are using a shared-pointer to const to refer to the implementation. The details are explained in Sean Parent’s talk. We get copy-on-write and value-semantics out of it for free (Magic!). I chose it here, because it eliminates all the boiler-plate for copy/move construction/assignment.
A working example of the code is available here.
Multiple Concepts
The problem arises when we want to merge two existing interfaces. For example, suppose there is a second concept: A door-opener, short Opener. I.e. a thing that opens doors. In some places of our code an Opener will be sufficient, in some other places we only need a Greeter, but in some places we need to first open the door for someone and then greet them:
How do we create
OpenerAndGreeter
? Well, we can just create a whole new class for it and copy-paste the Opener, and Greeter parts into it. Like so:But this is not ideal. It would be much better if we could take an existing Greeter concept, and an existing Opener concept, and just merge the two together.
A working example of the code is available here.
Dissecting Type-Erasure
Before we get there we need to understand what our type-erasure class actually does. So let’s take the Greeter apart.
First, it defines an abstract base class Concept. This is very specific to the Greeter. But, it has nothing to do with type-erasure. So, we pull it out.
Second, there is the model of that concept. This actually does two things: It holds an arbitrary value, and it passes the concept’s interface through to that value. So, let’s separate them.
Next, Greeter is also a container that refers to a concept, and initializes it with a model. This is very specific to type-erasure, but has nothing to do with greeting people.
And after all this hacking and slashing there is only one bit left. Namely, the external interface that passes calls through to the container.
Great! We started out with a perfectly well functioning class and took it apart into tiny pieces. Now we need to reassemble them and make sure that it still works. But don’t forget, the goal of this exercise is to make concepts mergeable — automatically. Hence, we need an automated way to assemble all the pieces that we created. So, it’s time for some template magic.
Automated Type-Erasure
Above pieces fall into two categories: One, there are pieces that define the Greeter’s interface, and two, there are pieces which define how to hold and call objects of arbitrary types. On the holding and calling side we find
Holder
, andContainer
, which are implementation details of our type-erasure container; whereasConcept
,Model
, andExternalInterface
are details of a Greeter. To keep things in order we will collect the Greeter parts in a super type that we callGreeterSpec
.At this stage we can write a template class that assembles all these pieces together and constructs a type-erasure container for an arbitrary spec. It will take the spec’s
ExternalInterface
template, and instantiate it with a container for the spec’s concept, and model. It will also pull in the base-classes constructor, so that we can still construct it from objects of arbitrary types.As the last line demonstrates, the Greeter itself is nothing but a TypeErasure of a certain spec.
Again, a working example of the code is available here.
Merging Concepts
Now, with all that machinery backing us, we can tackle the original problem: How to merge two concepts? We have a tool that creates a type-erasure class out of an arbitrary spec. And, we assume that we already have a
GreeterSpec
, and anOpenerSpec
that define those two concepts. What we need is a tool to automatically merge two specs into one. Let’s approach this component by component.How do we merge
Concept
classes, i.e. interfaces? In C++ we do this by multiple inheritance:How about the Models? The model is a template class that takes a holder as a template parameter and then inherits from said holder, thus becoming a holder itself. So, we can take
SpecB
, and the holder, and merge them into one class. This new class will itself be a holder. Next, we takeSpecA
, and that new holder, and merge them to get our final merged Model. There is one nifty detail, though: We need to use virtual inheritance for the concepts. The reason is thatConceptA
, andConceptB
will enter the mergedModel
through the mergedConcept
, but also through the models of the two specs.The external interfaces are merged the same way, just without the concepts:
Finally, to construct a merged spec we take all the above items and wrap them in a template class, that takes two specs:
With this it is trivial to create a type-erasure that merges two concepts:
And with just a little bit more of template magic it is even possible to merge two existing type-erasure classes. So, with all the above we write the following code:
Done!
And this last code example is available here.
Conclusion & Outlook
We find that it is indeed possible to merge two existing type-erasure classes into one that has a common interface. And what’s more, we can do it fully automatically and in just one line of code. The costly bit is to define the original type-erasure classes. For each one we need to define a spec class, and manually define the interface. The reason is that C++ does not support introspection. On the other hand, these specs follow a fairly strict scheme and it should be quite possible to produce them through tooling, or possibly even a macro.
Another possible issue is the inheritance pattern for
Model
, andExternalInterface
. Due to the chaining of base classes we introduce user specified names into the classesHolder
, andContainer
. The methodget
could be shadowed by a user method. The library code does actually contain more template magic to avoid this problem. A template meta-function peels layers of derived classes off until it arrives at the actualHolder
, orContainer
class. An external getter function is provided for the user, which makes sure to call the correct getter method. An obscured name of the internal getter method provides further protection.The full code is available here. Please feel invited to try it out and give me your feedback. Also, since this is my first blog post, any criticism is very welcome.
Thanks for reading!
Unfortunately, I have not yet figured out how to add comments to github pages. For the moment I would like to defer any discussion to Reddit. I apologize for the inconvenience.
via Andreas Herrmann
September 12, 2024 at 09:01AM
The text was updated successfully, but these errors were encountered: