Reputation: 8699
In our code with have a class hierarchy with virtual functions. We have different “diagrams” which produce a fixed bunch of numbers based on the same sort of parameters. The details are different, but the behavior is the same.
We are happy with the dynamic polymorphism as a pattern, we are not so fond of all the boilerplate code that we have to write in C++11 in order to achieve it. Mostly I am frustrated by the repetition in the constructors.
The class hierarchy looks as follows:
There is one base, two intermediate and a lot of children. The base and the intermediate have non-trivial constructors, the children do not require any more parameters. Yet we have to produce a constructor in the children because the default constructor would not properly construct the intermediate.
This is the code, stripped down to constructors, destructors and members only. The
Diagram
class:
class Diagram {
public:
Diagram(std::vector<CorrInfo> const &corr_lookup)
: corr_lookup_(corr_lookup) {}
virtual ~Diagram() {}
private:
std::vector<CorrInfo> const &corr_lookup_;
};
The intermediate DiagramNumeric<>
class:
template <typename Numeric_>
class DiagramNumeric : public Diagram {
public:
using Numeric = Numeric_;
DiagramNumeric(std::vector<CorrInfo> const &_corr_lookup,
std::string const &output_path,
std::string const &output_filename,
int const Lt)
: Diagram(_corr_lookup),
output_path_(output_path),
output_filename_(output_filename),
Lt_(Lt),
correlator_(corr_lookup().size(), std::vector<Numeric>(Lt, Numeric{})),
c_(omp_get_max_threads(),
std::vector<std::vector<Numeric>>(
Lt, std::vector<Numeric>(corr_lookup().size(), Numeric{}))) {}
private:
std::string const &output_path_;
std::string const &output_filename_;
int const Lt_;
std::vector<std::vector<Numeric>> correlator_;
std::vector<std::vector<std::vector<Numeric>>> c_;
};
And one of the children, here C2c
:
class C2c : public DiagramNumeric<cmplx> {
public:
C2c(std::vector<CorrInfo> const &corr_lookup,
std::string const &output_path,
std::string const &output_filename,
int const Lt);
};
C2c::C2c(std::vector<CorrInfo> const &corr_lookup,
std::string const &output_path,
std::string const &output_filename,
int const Lt)
: DiagramNumeric<cmplx>(corr_lookup, output_path, output_filename, Lt) {}
When I need to add another argument to the constructor of the intermediate, I have to
change the DiagramNumeric<>
declaration, and every child declaration and definition.
This makes 2 * N + 1
changes with N
children and feels awful.
I thought about making a struct
as DiagramNumeric<>::CtorParams
and that is just
passed through. The client code where the C2c
is instantiated needs to be adapted, but
we actually pass only different corr_lookup
parameters, the remaining three are always
the same.
Is there some mechanism with which we can cut away a significant portion of this boilerplate code?
Upvotes: 0
Views: 117
Reputation: 22890
We are happy with the dynamic polymorphism as a pattern,.... I am frustrated by the repetition in the constructors.
This isn't some c++11 fault. The issue come directly from dynamic polymorphism. And particularly from the fact that each child class is required to construct its parents.
The alternative you have is dependency injection + static polymorphism :
Diagram
template as argumentDiagramNumeric<cmplx>
anymore, but merely receive one as argument which is copied to a member variable. (dependency injection)You will end up with
class C2c {
public:
explicit C2c(const DiagramNumeric<cmplx> &diagram_numeric)
: diagram_numeric_(diagram_numeric) {}
private :
DiagramNumeric<cmplx> diagram_numeric_;
};
Let say you want to add another argument. In the best case, that's a O(1) change in terms of dependent classes. In the worst case, it's back to 2*N+1 changes if you have a builder function such as
C2c build(std::vector<CorrInfo> const &corr_lookup,
std::string const &output_path,
std::string const &output_filename,
int const Lt)
{
return C2c(DiagramNumeric<cmplx>(corr_lookup, output_path, output_filename, Lt));
}
Upvotes: 1