Martin Ueding
Martin Ueding

Reputation: 8699

Reduce constructor boilerplate code in class hierarchy

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:

enter image description here

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

Answers (1)

UmNyobe
UmNyobe

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 :

  1. You have classes and methods taking a Diagram template as argument
  2. You don't have a Diagram class anymore (static polymorphism)
  3. C2c and the other classes don't construct a DiagramNumeric<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

Related Questions