0

I have a virtual parent class for collecting reports with its associated report struct. The reports should be rendered as a JSON string in the end, so I'm using https://github.com/nlohmann/json to help me with that.

I create different different child classes to generate such reports of the respective child report structs, and the challenge is that each child report may have slightly different fields, but inherit some from the parent. I have the macros that are needed to convert the structs to JSON representation, defined per report type. This is the code so far:

/**
 * Compile with nlohmann json.hpp
 */

#include <iostream>
#include <vector>
#include <memory>

#include "json.hpp"
using json = nlohmann::json;

struct Report {
  // make abstract
  virtual ~Report() {}
  std::string type = "main_report";
  int foo = 0;
};

struct ChildAReport : public Report {
  std::string type = "child_a_report";
  int bar = 1;
};

struct ChildBReport : public Report {
  std::string type = "child_b_report";
  int baz = 2;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ChildAReport, type, foo, bar)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ChildBReport, type, foo, baz)

class Parent {
protected:
  std::vector<std::shared_ptr<Report>> reports;
  virtual void run() = 0;
  virtual std::string get_report() = 0;
};

class ChildA : public Parent {
public:
  virtual void run() override
  {
    ChildAReport r;
    r.foo = 1;
    r.bar = 2;
    reports.push_back(std::make_shared<ChildAReport>(r));
  }

  std::string get_report() override {
    std::shared_ptr<Report> r = reports.back();
    std::shared_ptr<ChildAReport> cr = std::dynamic_pointer_cast<ChildAReport>(r);
    json r_json = *cr;
    return r_json.dump();
  }
};

class ChildB : public Parent {
public:
  virtual void run() override
  {
    ChildBReport r;
    r.foo = 1;
    r.baz = 3;
    reports.push_back(std::make_shared<ChildBReport>(r));
  }

  std::string get_report() override {
    std::shared_ptr<Report> r = reports.back();
    std::shared_ptr<ChildBReport> cr = std::dynamic_pointer_cast<ChildBReport>(r);
    json r_json = *cr;
    return r_json.dump();
  }
};

int main(int argc, char *argv[])
{
  ChildA ca = ChildA();
  ca.run();
  std::cout << ca.get_report() << std::endl;

  ChildB cb = ChildB();
  cb.run();
  std::cout << cb.get_report() << std::endl;
}

The code is compiled with json.hpp and no further dependencies.

I am expecting this output:

{"bar":2,"foo":1,"type":"child_a_report"}
{"baz":3,"foo":1,"type":"child_b_report"}

Now, in order to actually generate the JSON, I use the get_report() method. I learned that I have to downcast the pointer to the Report struct to the actual ChildA or ChildB struct, because otherwise it won't properly convert to JSON. This is tedious; as you can see, the code is repeated almost verbatim in every possible child class. The run() function is not the problem – here's where all sorts of magic happens, which differs on a per-class basis.

Is there a way I can pull this up to the parent class without having to explicitly specify the type of cast that is to be made before converting to JSON? Ideally this could be inferred depending on the actual type that get_report() is being run on ...

slhck
  • 36,575
  • 28
  • 148
  • 201
  • 3
    I think you are looking at the [visitor pattern](https://refactoring.guru/design-patterns/visitor). This would avoid the casting of pointers. – Daniel Dearlove Jun 30 '21 at 11:45
  • Ah, I admit I was asking for a particular solution I had in mind rather than considering completely different architectural choices. The visitor patterns seems a bit complex though. I would still have to create a new method for every child class that does the export, no? (Akin to `visitCircle` vs `visitRectangle` etc.) – slhck Jun 30 '21 at 11:53

2 Answers2

1

Also an idea could be to make the parent class a templated class, which uses the child report type as template parameter:

#include <iostream>
#include <vector>
#include <memory>

struct Report {
  // make abstract
  virtual ~Report() {}
  std::string type = "main_report";
  int foo = 0;
};

struct ChildAReport : public Report {
  std::string type = "child_a_report";
  int bar = 1;
};

struct ChildBReport : public Report {
  std::string type = "child_b_report";
  int baz = 2;
};

template<typename REPORT>
class Parent 
{
public:
  std::string get_report()
  {
    auto r = reports.back();
    return r->type;
  }
  virtual void run() = 0;

protected:
  std::vector<std::shared_ptr<REPORT>> reports;
};

class ChildA : public Parent<ChildAReport> {
public:
  virtual void run() override
  {
    ChildAReport r;
    r.foo = 1;
    r.bar = 2;
    reports.push_back(std::make_shared<ChildAReport>(r));
  }
};

class ChildB : public Parent<ChildBReport> {
public:
  virtual void run() override
  {
    ChildBReport r;
    r.foo = 1;
    r.baz = 3;
    reports.push_back(std::make_shared<ChildBReport>(r));
  }
};

int main()
{
  auto ca = ChildA();
  ca.run();
  std::cout << ca.get_report() << std::endl;

  auto cb = ChildB();
  cb.run();
  std::cout << cb.get_report() << std::endl;

  return 0;
}

Edited answer, because pure virtual method is not needed anymore

Update for your comment:

Create a base class (interface) for parent to be used in your example:

class IGrandparent
{
public:
  virtual std::string get_report() = 0;
  virtual void run() = 0;
};

template<typename REPORT>
class Parent : public IGrandparent
{
public:
  std::string get_report() override 
  {
    auto r = reports.back();
    return r->type;
  }
protected:
  std::vector<std::shared_ptr<REPORT>> reports;
};

int main()
{
    std::unique_ptr<IGrandparent> childAInDisguise = std::make_unique<ChildA>();
    std::unique_ptr<IGrandparent> childBInDisguise = std::make_unique<ChildB>();

    childAInDisguise->run();
    std::cout << childAInDisguise->get_report() << std::endl;

    childBInDisguise->run();
    std::cout << childBInDisguise->get_report() << std::endl;

    return 0;
}
RoQuOTriX
  • 2,871
  • 14
  • 25
  • 1
    Interesting! Would I still have to provide implementations of `std::string get_report() override` for each child class? Or couldn't that be moved into one parent class function? – slhck Jun 30 '21 at 12:08
  • Yes you are right, that is even better, did not notice that! – RoQuOTriX Jun 30 '21 at 12:12
  • @slhck also what is bad design in my eyes is changing the access specifier of virtual methods. You declared the methods as protected and override them as public. Make them public in the base class because you want them to be used from outside – RoQuOTriX Jun 30 '21 at 12:14
  • Now, assume that from the outside, I used to have a `std::unique_ptr` and instantiate that at runtime, depending on the chosen type, with `std::make_unique();`. Now this doesn't work anymore because it says "argument list for class template is missing" on the `unique_ptr` when I assign `std::make_unique>();`. How would I have to modify that? – slhck Jun 30 '21 at 12:16
  • 1
    I figured it out. I need to provide a virtual `BaseParent` class that does not rely on templates and derive from that. Thank you for the answer! – slhck Jun 30 '21 at 12:24
  • @slhck yes I added an example the same time you figured it out! – RoQuOTriX Jun 30 '21 at 12:24
1

One thing you can do it's to have a template in the base class Parent but this implies to remove the virtual keyword so Parent class won't be a interface anymore. This it's only the easy way to do it for making get_report less tedious, if Parent class has to be an interface this solution is not valid.

If this helps you it would be like this:

class Parent {
protected:
  std::vector<std::shared_ptr<Report>> reports;
  virtual void run() = 0;
  template <class T>
  std::string get_report_base() 
  {
    std::shared_ptr<Report> r = reports.back();
    std::shared_ptr<T> cr = std::dynamic_pointer_cast<T>(r);
    json r_json = *cr;
    return r_json.dump();
  }
};

And the children will cal get_report_base:

class ChildA : public Parent {
public:
  virtual void run() override
  {
    ChildAReport r;
    r.foo = 1;
    r.bar = 2;
    reports.push_back(std::make_shared<ChildAReport>(r));
  }

  std::string get_report()  {
    return get_report_base<ChildAReport>();
  }
};
Ojotuno
  • 21
  • 6