1

Is it possible to perform double dispatch with runtime polymorphism?

Say I have some classes, and some of those classes can be added/multiplied/etc., and I want to store those dynamically within another class that performs type erasure at runtime. Then say I want to perform basic operations on the data held within that class.

The way to handle this (as far as I'm aware) is to use double dispatch to specialize the operation. However, all of the solutions I have encountered rely on the fact that you have a numerable amount of types, and then use virtual function calls or dynamic_casts, if-else, and RTTI to deduce the type at runtime. Because the data held within the class isn't known until runtime, I can't create a bunch of virtual methods or do a brute force check on the types. So I figured the visitor pattern would be the best solution, but even then, I can't seem to get whether or not this is possible.

I have a wrapper class that holds a smart pointer to a nested polymorphic class to implement the type erasure and runtime polymorphism, but I can't figure out if it's possible to use the visitor pattern to do double dispatch on this.

Note that the code below is incomplete, it just shows my thought process.

class Wrapper {
private:
  class Concept;
  template<typename T> class Model;

  class BaseVisitor {
  public:
    virtual ~Visitor() = default;
    virtual void visit(Concept &) = 0;
  };

  template<typename T>
  class Visitor : public BaseVisitor {
  private:
    T first_;
  public:
    Visitor(T first) : first_(first) {}

    virtual void visit(Concept &other) override {
      // perform addition
    }
  };

  class Concept {
  public:
    virtual ~Concept() = default;

    virtual void add(Concept &m) const = 0;
    virtual void accept(BaseVisitor &visitor) const = 0;
  };

  template<typename T>
  class Model final : public Concept {
  private:
    T data_;

  public:
    Model(T m)
        : data_(m) {}

    virtual void add(Concept &m) const override {
      Visitor<T> v(data_);
      m.accept(v);
    };
    virtual void accept(BaseVisitor &visitor) const override {
      visitor.visit(*this);
    };
  };

  std::shared_ptr<const Concept> ptr_;

  // This isn't right, it just illustrates what I'm trying to do.
  // friend Something operator+(Wrapper &lhs, Wrapper &rhs) {
  //   return (*lhs.ptr_).add(*rhs.ptr_);
  // }

public:
  template<typename T>
  Wrapper(T value) : ptr_(std::make_shared<Model<T>>(value)) {}
};

I've looked into implementing double dispatch using function pointers, template specialization, and static type IDs as well, but I can't seem to figure out how to make it work.

Is this even possible?


EDIT

Based on the comments below, in order to be more specific and to give a little more background, I am using templated classes that use template functions to perform basic operations like addition and multiplication. However, I would also like to store those templated classes within a vector, hence the type erasure. Now, if I wanted to do operations on those classes after I perform the type erasure, I need some way to deduce the type for the templated function. However, since I can't easily get the internal held type back from the Wrapper, I am hoping that there is a way I can call the correct template function on the data held within the Wrapper::Model<T> class, whether that is a visitor pattern, static type IDs, whatever.


To be even more specific, I am working with classes to do delayed evaluation and symbolic computations, meaning I have classes such as Number<T>, which can be Number<int>, Number<double>, etc. and classes such as Variable, Complex<T> and all of the TMP combinations for various operations, such as Add<Mul<Variable, Variable>, Number<double>>, etc.

I can work with all of these fine at compile-time, but then I need to be able to store these in a vector -- something like std::vector<Wrapper> x = {Number<int>, Variable, Add<Number<double>, Variable>};. My best guess at this was to perform type erasure to store the expressions inside the polymorphic Wrapper. This serves double-duty to enable runtime parsing support of symbolic expressions.

However, the functions I wrote to handle the addition, such as

template<typename T1, typename T2>
const Add<T1, T2> operator+(const T1 &lhs, const T2 &rhs)
{ return Add<T1, T2>(lhs, rhs); }

can't accept Wrapper and pull the type out (due to the type erasure). I can, however, insert the Wrapper into the Add expression class, meaning I can carry around the hidden types. The problem is when I actually get down to evaluating the result of something like Add<Wrapper, Wrapper>. In order to know what this comes out to, I'd need to figure out what's actually inside or to do something along the lines of double dispatch.

The main problem is that the examples for double dispatch that most closely match my problem, like this question on SO, rely on the fact that I can write out all of the classes, such as Shapes, Rectangles. Since I can't explicitly do that, I'm wondering if there's a way to perform double dispatch to evaluate the expression based on the data held inside the Model<T> class above.

Adam
  • 303
  • 3
  • 11
  • Sounds like you might want something like `std::any` that you can shove anything into, but then your "visitors" will need to know exactly what types they expect to visit. – AndyG Feb 17 '19 at 00:15
  • "Because the data held within the class isn't known until runtime," how isn't the set of all possible kinds of data not known at compile time? There are answers to this, but the exact, practical and concrete details of such an answer can have massive and surprising differences in what you need to do. – Yakk - Adam Nevraumont Feb 17 '19 at 00:17
  • Or, in short, you are trying to describe a problem using abstract terms which *do not map well to the solution space*. You need to be more concrete so a solution for that concrete problem can be generated, rather than abstractly talking about a set of problems whose solutions vary from "simple" to "halt-hard" without the vocabulary to separate the two kinds of issues! – Yakk - Adam Nevraumont Feb 17 '19 at 00:22
  • @Yakk-AdamNevraumont, I may very well be using incorrect or imprecise terminology. I'm no expert. So I'll try to clarify my statement by saying I am hoping to store a templated class within the type erasure class, which is why I make the claim that I won't know all of the types that can go into the wrapper. – Adam Feb 17 '19 at 00:33
  • @adam Again, in any fixed program you do know every type you pass to the template; so do you kniw all types or not? Sexond, please strip out this abstraction. You want a polymorphic value type, right? You want to support `c=a+b`, where `c` must store the result of adding the types within `a` and `b`? Or did I miss something in your abstraction? – Yakk - Adam Nevraumont Feb 17 '19 at 03:02
  • @Yakk-AdamNevraumont, see the edits above. Please let me know if this helps answer your follow-up question. If not, I'd be happy to add more information or clarify further to help make the problem more specific. – Adam Feb 17 '19 at 03:48
  • If you have say N types to multiply, you need N^2 multiplication methods. You cannot fit N^2 methods in an O(n) long source code. You need to write N^2 multiply methods. There's no way around this. Once you have these methods there are ways to dispatch to them, google e.g. "acyclic visitor". – n. m. could be an AI Feb 17 '19 at 04:18
  • @n.m No. You can generate O(n^2) code with template metaprogramming. Or std visit. – Yakk - Adam Nevraumont Feb 17 '19 at 14:32
  • @Yakk-AdamNevraumont Please explain how you would generate all multiplication routines for these types: `int, float, rational, bigint, rational` with template metaprogramming (assume you don't want to use the built-in multiplication but create your own with bit manipulation or whatever). – n. m. could be an AI Feb 17 '19 at 15:34
  • @n.m. std visit( [](auto&& a, auto&& b){return adl_mult(a,b); }var1, var2 ); now you have a function overloaded on both at once. Test if both are built in or same type; use built in times. If either are rational, route to other set of overloads. Etc. If you can describe what you mean in less than O(n^2) you can probably program it. – Yakk - Adam Nevraumont Feb 17 '19 at 17:18
  • @Yakk-AdamNevraumont If you are ranking the types and perform arithmetic promotions from lower- to higher-ranking type, that's a quite different problem. OP's language may or may not have prmotions. How would you perform promotions anyway? There's still O(N^2) of them and you need to describe them somehow. You don't have to do that for builtin types only because C++ has already done O(N^2) work for you. – n. m. could be an AI Feb 17 '19 at 17:31
  • @n.m., to be honest, I'm still working on a solution for some of what you describe above, but my current idea is to use a templated [common_type](https://en.cppreference.com/w/cpp/types/common_type) method for basic types, and implicit conversions to one `big` type when I get there. – Adam Feb 17 '19 at 22:54
  • @Adam if you have code with statically known types working for any pair of types, then you can use the acyclic visitor technique which dispatches on pairs (or longer tuples even) of typeids. – n. m. could be an AI Feb 18 '19 at 09:03
  • @n.m., I tried playing with this for a week, but I still can't seem to figure out how you intended for the acyclic visitor to work for double dispatch using pairs of type IDs. Is there a way you could link to an example? – Adam Feb 26 '19 at 23:27
  • You create a static object of type `map>, Base* (*func)(Base*, Base*)>`, call it dispatch_map. It stores functions like `add_Int_Double`, all with the same signature. Then there's just one global function `Base* Add(Base* arg1, Base* arg2)` which just says `return dispatch_map.find({types})(arg1, arg2)`, where `types` is `pair{type_index(typeid(*arg1)), type_index(typeid(*arg2))}`. The actual function say `add_Int_Double` accepts pointers to Base (use smart pointers if you want) and downcasts them to actual types. – n. m. could be an AI Feb 27 '19 at 04:56

0 Answers0