1

Is there a trick to decide which derived class is used in a templated class hierarchy, or at least a way to call the correct function? I've put together a simple snippet, which describes the issue.

  

class Visitable {
public:
  template <typename Visitor> void accept(Visitor &visitor) {
    std::cout << "Base";
  }
};
    
    
class Base : public Visitable {};
    
class DerivedA : public Base {
public:
  template <typename Visitor> void accept(Visitor &visitor) {
    std::cout << "DerivedA";
  }
};

class DerivedB : public Base {
public:
  template <typename Visitor> void accept(Visitor &visitor) {
    std::cout << "DerivedB";
  }
};

template <class T> class Visitor {
public:
  virtual void visit(T &) = 0;
};

template <typename... Ts> class Visitor_N : public Visitor<Ts>... {
public:
  using Visitor<Ts>::visit...;
};

// this is a smiplified Visitor, just to have one at place for the example.
// Real visitors are more complex and do different stuff for different types.
class SampleCounter : public Visitor_N<Base, DerivedA, DerivedB> {
public:
  int i = 0;
  void visit(Base &t) override { i++; }
  void visit(DerivedA &t) override { i++; }
  void visit(DerivedB &t) override { i++; }
};

int main() {

  /**
   * The following few lines off course work.
   */
  SampleCounter counter;
  DerivedA derivedA;
  derivedA.accept(counter); // It knows the correct type as I call it from
                            // DerivedA directly

  /**
   * Here it calls the accept function of Base, as it should be according to c++ rules.
   */
  Base *base = new DerivedA();
  base->accept(counter);
  delete base;
}


I've tried to implement Base in a kind of CRTP form, so the base knows all classes implementing it. Then maybe find the type by using covariant return types (described here) and call accept by explicitly calling the right accept function. With this approach I ran into different kinds of Incomplete Type issues.

Another idea was to apply a kind of pointer (type of derived class) in the base class to decide which class I'm working with. Couldn't get this to work either.

Other ideas I'm not sure are possible:

Type map

is there a way to implement a compile time type map (maybe short -> typename) and automatically add entries and associated classes? Maybe like the following (in the style of cp_map):

template <typename ... Ps>
class cp_map { /* code like described in https://stackoverflow.com/a/19503045/1893976 */}

template<typename Derived>
class base : public Visitor, public Derived {
  template <typename Visitor> void accept(Visitor &visitor) {
    // not real cpp code:
    auto type = cp_map::getValue(this->getType())
    // call accept on right type somehow
  }
}

Enum

Following the above type map, would there be a way to implement an enum in a variadic template way, basically doing the same stuff as the type map? Kind of...:

template <typename ... Ts>
enum type_map {
   Ts...
}

I guess it is somehow possible to find the derived type (as other SO-answers and questions suggest). But I need a little help in this more template-centric code.


Edit: Why (complex) template classes

I began with a default visitor pattern: Visitor than had a method for each node type:

class ExampleVisitor : public Vistor {
  virtual void visitDerivedA(){ /*... */}
  virtual void visitDerivedB(){ /*... */}
  // ...
}

This pattern was good and sufficient as long as I only had one AST tree with limited AST-Node types and vistors. As the application was getting bigger with multiple trees of different types I've splitted the tree in very general (templated) node classes. Even a "handler" for parent-functionality was outsourced as it does always the same thing, but for different types in different trees.

This led to Node-Definitions like the following:

class TypeDefinitionNode
  : public AstNode<TypeDefinitionNode> // inherits base class 
  , public CompositeType<
      TypeDefinitionNode, 
      BlockNode // type of child nodes
      >
  , public Parent<
      TypeDefinitionNode,
      DocumentNode // what type has the parent of this node
     >
{ /** ... */ };

The complexity of defining a node type isn't that complex and it's very clear what functionality the class has (through inheritance). The main benefit of it all was the easier access of traversing the tree as all types are clear at compile time. This allows eg. the following (one example out of many):

 auto p = f.getParentOfType<Document>();

For many such functions, there's no longer a visitor required. But it still got the visitor pattern for existing algorithms.

Felix
  • 2,531
  • 14
  • 25
  • 2
    I don't fully understand why you are doing what you do: what do you want to archieve (and why)? What's the input and the expected output? Why is standard function overloading not good? – JHBonarius Oct 28 '20 at 17:42
  • 1
    OK, I get it now... There's no option to make the use a class template? I.e. `DerivedA derivedA;`? Something like [this](https://godbolt.org/z/b77YE5). But maybe you're just making things needlessly complex. So I again ask: why do you want this construction? What do you want to achieve? – JHBonarius Oct 28 '20 at 18:00
  • It kind of grew this way. I built a parent class and a composition class which allowed me to traverse multiple AST trees but now I stuck when there are children not of one specific type. – Felix Oct 28 '20 at 18:14
  • 1
    That doesn't really answer my question: *why* did it grow this way? What do you want to achieve? What's `Base`, `DerivedA/B`. Why do they need a `visitor`? Why do all `visit` increase a local counter? To me this is a [XY problem](http://xyproblem.info/): you're asking us to fix your solution, but we could maybe help you find a better solution. Your current code can just be reduced to [this](https://godbolt.org/z/z58Pfo). – JHBonarius Oct 28 '20 at 18:19
  • Generic visitor pattern: https://stackoverflow.com/a/11802080/1863938 Not sure about what you're looking for with "type map", but are you maybe looking for [`std::variant`](https://en.cppreference.com/w/cpp/utility/variant), which store one of N types using a union and an integer ID? – parktomatomi Oct 28 '20 at 18:24
  • The ExampleCounter was just an example to have a visitor at place and to make the example as small as possible. I've updated my answer to make it clear how I came to this situation. – Felix Oct 28 '20 at 19:48
  • 1
    The normal visitor pattern doesn't work the way you are trying to implement. `accept` should be virtual and of course not a template. Why are you trying to make it a template? It doesn't make any sense. – n. m. could be an AI Oct 28 '20 at 19:51
  • @Felix I forgot to mention that the overloaded{} collection of lambdas also works with auto. That can help quite a bit to limit the amount of code written. Keep the lambda with auto last for best result. – Michaël Roy Jan 12 '21 at 12:59

1 Answers1

0

Your example is a bit contrived.... the visitor pattern is made for and works best with std::variant<>.. Have you tried visitor pattern #4 from the example in the std::visit docmentation at cppreference? (https://en.cppreference.com/w/cpp/utility/variant/visit)

// needed declaration cut & pasted from the example at cppreference 
// helper type for the visitor #4
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

// using variant instead of class hierarchy.
struct ClassA {  int member_of_a; };
struct ClassB {  int member_of_b; };

using Variant = std::variant<ClassA, ClassB>;

Variant v = ClassA{42};

int n = 0;
n += std::visit(overloaded{
    [](const ClassA& a) {  return a.member_of_a; },
    [](const ClassB& b) {  return b.member_of_b; }
  }, v);

// or, with additional parameters, you could call member functions or free functions as well. 
std::visit(overloaded{
    [](const ClassA& a, int x) {  a.member_of_a = x; },
    [](const ClassB& b, int x) {  b.member_of_b = x; }
  }, v, 10);

// This also works with class hierarchy.
struct ClassC : ClassB { int member_of_c; };

using Variant2 = std::variant<ClassA, ClassB, ClassC>;
Variant2 v2{ClassC{}};

n += std::visit(overloaded{
    [](const ClassA& a) {  return a.member_of_a; },
    [](const ClassB& b) {  return b.member_of_b; },
    [](const ClassC& c) {  return c.member_of_c; }
  }, v);

// or, using same processing for ClassB and ClassC objects, which are related. 
n += std::visit(overloaded{
    [](const ClassA& a) {  return a.member_of_a; },
    [](const ClassB& b) {  return b.member_of_b; }
  }, v);

You'll note that ClassA and ClassB are totally independant from each other... That's when the visitation paradigm really shines.

This means it works well with templates.

temptate <typename T>
struct Template { T value; }

using Variant = std::variant<Template<int>, Template<float>, Template<double>>;

Variant v = Template<int>{};

// added some multiplexing, just to spice up the example a bit.
std::visit(overloaded{
    [](const Template<int>& a, int x, float) {  a.value += x; },
    [](const Template<float>& b, int, float y) {  b.value += y; },
    [](const Template<double>& b, int x, float y) {  b.value += std::sin(x * double(y)); }
  }, v, 10, 42.f);

If you do not want to work with variants, then you'll have to use class hierachy and call virtual functions to get the effect you want. No need for visitation at all in this case but you'll be limited to the hierarchy for the objects that your code will work with.

struct Base { virtual int inc() = 0; }
struct ClassA {  int int() override { return 42; } };
struct ClassB {  int int() override { return 10; } };

std::unique_ptr<Base> p = std::make_unique<ClassA>();

int n = 0;
n += p->inc();

For storage in std containers, you can use std::variant<> as you would any regular type, with the usual restrictions on default construction, copy, move, etc... resting on your template class.

Michaël Roy
  • 6,338
  • 1
  • 15
  • 19