patterncppModerate
Compile time decorator pattern in C++ with templates
Viewed 0 times
compilewithtimetemplatesdecoratorpattern
Problem
I'm doing some high energy physics modelling in C++. I have written code that implements class that score interactions of particles with detector material.
Here is a base class:
Function
Since modeling experimental data is quite hard, I need to be able to add some features to these scorers and I'll need to be able to easily switch these features on and off. I decided to implement these features using a decorator pattern. Here is decorator base class.
Here are two implementations of these decorators:
```
template
class FixZToFirstInteractionScorer: public virtual ScorerDecorator{
private:
bool fixed_in_this_event=false;
double position_of_first_intraction=0;
public:
FixZToFirstInteractionScorer(Scorer scorer): ScorerDecorator(scorer){}
virtual ~FixZToFirstInteractionScorer(){}
virtual void SetEvent(size_t new_incident){
fixed_in_this_event=false;
ScorerDecorator::SetEvent(new_incident);
}
virtual void RegisterHit(const XYZHit hit){
if(!fixed_in_this_event){
fixed_in_this_event=true;
position_of_first_intraction=hit.z;
}
XYZHit fixed_coords = hit;
fixed_coords.z-=position_of_first_intraction;
this->scorer.RegisterHit(fixed_coords);
Here is a base class:
class XYZScorer: public virtual AbstractScorer{
public:
XYZScorer(double material_rad_len=-1):
AbstractScorer(material_rad_len){;}
virtual void RegisterHit(const XYZHit hit)=0;
};Function
RegisterHit registers single interaction between particle and detector. There are many scorers in this program. Since modeling experimental data is quite hard, I need to be able to add some features to these scorers and I'll need to be able to easily switch these features on and off. I decided to implement these features using a decorator pattern. Here is decorator base class.
template
class ScorerDecorator: public virtual XYZScorer{
protected:
Scorer scorer;
public:
ScorerDecorator(Scorer scorer): XYZScorer(scorer.material_rad_len), scorer(scorer){;}
virtual ~ScorerDecorator(){}
virtual void SetEvent(size_t new_incident){
scorer.SetEvent(new_incident);
}
virtual void CloseAfterRunEnd(){
scorer.CloseAfterRunEnd();
}
};Here are two implementations of these decorators:
```
template
class FixZToFirstInteractionScorer: public virtual ScorerDecorator{
private:
bool fixed_in_this_event=false;
double position_of_first_intraction=0;
public:
FixZToFirstInteractionScorer(Scorer scorer): ScorerDecorator(scorer){}
virtual ~FixZToFirstInteractionScorer(){}
virtual void SetEvent(size_t new_incident){
fixed_in_this_event=false;
ScorerDecorator::SetEvent(new_incident);
}
virtual void RegisterHit(const XYZHit hit){
if(!fixed_in_this_event){
fixed_in_this_event=true;
position_of_first_intraction=hit.z;
}
XYZHit fixed_coords = hit;
fixed_coords.z-=position_of_first_intraction;
this->scorer.RegisterHit(fixed_coords);
Solution
I might have a number of minor stylistic comments but this is not important. What is important is the desired function
which can be used like
This is very close to the desired syntax (maybe better), generic with respect to number and type of arguments, and inlinable.
Here is a live example, where I've thrown some arbitrary code to fill in the missing pieces of your code so that the whole thing compiles.
Attempt 2
I have now realized that the entire approach is wrong. We have a number of class templates and we want to apply them to a given object sequentially, constructing a nested object. To do this, we have a list of the class templates and we ask a single function
This is not right. The class templates themselves should not be used directly. We should have a helper function to wrap a given object into each class individually, exactly what
to be used like
See new live example, where I have both approaches and a validation that they produce the same result. In fact, I've discovered a mistake in my first attempt, which I have now corrected.
I think this is cleaner and probably easier to use. Its implementation is easier to understand (it's not so "magical" as the first attempt) and certainly easier for the compiler (so, faster to compile). Each object is created at the right time when its type is known, and no recursion is needed to find all unknown return types of deeply nested function calls as with
Of course, this means we have to write one more function for each new scorer class template. But all these functions are so small and similar. If we are very lazy we could even write a macro for them.
Note that in this case we are also constructing the outermost
Attempt 3
Never giving up. New live example.
Without
It just works! Why?
We could even write a new
decorate():template
using result = typename std::result_of ::type;
template class E, template class... D>
struct decorate_impl
{
template (A)>>
E operator()(A&& a) const
{
return E(decorate_impl()(std::forward(a)));
}
};
template class E>
struct decorate_impl
{
template ::type>
E operator()(A&& a) const { return E(std::forward(a)); }
};
template class E, template class... D, typename A>
std::shared_ptr (A)>>)
decorate(A&& a)
{
return std::make_shared (A)>>>
(decorate_impl()(std::forward(a)));
}which can be used like
std::shared_ptr ptr =
decorate(scorer);This is very close to the desired syntax (maybe better), generic with respect to number and type of arguments, and inlinable.
Here is a live example, where I've thrown some arbitrary code to fill in the missing pieces of your code so that the whole thing compiles.
Attempt 2
I have now realized that the entire approach is wrong. We have a number of class templates and we want to apply them to a given object sequentially, constructing a nested object. To do this, we have a list of the class templates and we ask a single function
decorate() to do the entire job for us, using recursion.This is not right. The class templates themselves should not be used directly. We should have a helper function to wrap a given object into each class individually, exactly what
std::make_shared() does for std::shared_ptr (similarly make_tuple, make_pair etc.). So here it is:template
using decay = typename std::decay::type;
template
FixZToFirstInteractionScorer> fixZToFirstInteraction(S&& scorer)
{
return FixZToFirstInteractionScorer>(std::forward(scorer));
}
template
ApplyEnergyCutsScorer> applyEnergyCuts(S&& scorer)
{
return ApplyEnergyCutsScorer>(std::forward(scorer));
}
template
std::shared_ptr> share(S&& scorer)
{
return std::make_shared>(std::forward(scorer));
}to be used like
std::shared_ptr ptr =
share(fixZToFirstInteraction(applyEnergyCuts(scorer)));See new live example, where I have both approaches and a validation that they produce the same result. In fact, I've discovered a mistake in my first attempt, which I have now corrected.
I think this is cleaner and probably easier to use. Its implementation is easier to understand (it's not so "magical" as the first attempt) and certainly easier for the compiler (so, faster to compile). Each object is created at the right time when its type is known, and no recursion is needed to find all unknown return types of deeply nested function calls as with
decorate(). Maybe I was carried away by the "desired syntax" and didn't see this immediately.Of course, this means we have to write one more function for each new scorer class template. But all these functions are so small and similar. If we are very lazy we could even write a macro for them.
Note that in this case we are also constructing the outermost
Scorer before making the shared_ptr, which we didn't in the first attempt. The reason is that otherwise we would need two functions for each class template, one returning an object on the stack and another a shared_ptr. In case Scorer is a large object, a move constructor will ensure that we don't loose performance with the unnecessary object.Attempt 3
Never giving up. New live example.
Without
decorate(), without any helper functions (except share()), just this:std::shared_ptr ptr3 =
share(FixZToFirstInteractionScorer>(scorer));It just works! Why?
FixZToFirstInteractionScorer> expects an ApplyEnergyCutsScorer argument and we give it just a Scorer. But an ApplyEnergyCutsScorer is implicitly constructed from the Scorer. All this happens automatically without touching the code, and will work under arbitrary nesting.We could even write a new
decorate() around this idea, which would be purely metaprogramming now. I may do this, or not.Code Snippets
template <typename S>
using result = typename std::result_of <S>::type;
template <template <typename> class E, template <typename> class... D>
struct decorate_impl
{
template <typename A, typename N = result<decorate_impl<D...>(A)>>
E<N> operator()(A&& a) const
{
return E<N>(decorate_impl<D...>()(std::forward<A>(a)));
}
};
template <template <typename> class E>
struct decorate_impl <E>
{
template <typename A, typename N = typename std::decay<A>::type>
E<N> operator()(A&& a) const { return E<N>(std::forward<A>(a)); }
};
template <template <typename> class E, template <typename> class... D, typename A>
std::shared_ptr <E<result<decorate_impl<D...>(A)>>)
decorate(A&& a)
{
return std::make_shared <E<result<decorate_impl<D...>(A)>>>
(decorate_impl<D...>()(std::forward<A>(a)));
}std::shared_ptr<XYZScorer> ptr =
decorate<FixZToFirstInteractionScorer, ApplyEnergyCutsScorer>(scorer);template <typename S>
using decay = typename std::decay<S>::type;
template <typename S>
FixZToFirstInteractionScorer<decay<S>> fixZToFirstInteraction(S&& scorer)
{
return FixZToFirstInteractionScorer<decay<S>>(std::forward<S>(scorer));
}
template <typename S>
ApplyEnergyCutsScorer<decay<S>> applyEnergyCuts(S&& scorer)
{
return ApplyEnergyCutsScorer<decay<S>>(std::forward<S>(scorer));
}
template<typename S>
std::shared_ptr<decay<S>> share(S&& scorer)
{
return std::make_shared<decay<S>>(std::forward<S>(scorer));
}std::shared_ptr<XYZScorer> ptr =
share(fixZToFirstInteraction(applyEnergyCuts(scorer)));std::shared_ptr<XYZScorer> ptr3 =
share(FixZToFirstInteractionScorer<ApplyEnergyCutsScorer<Scorer>>(scorer));Context
StackExchange Code Review Q#47083, answer score: 11
Revisions (0)
No revisions yet.