Search code examples
c++variant

How to invoking a function on a variant without casting


I have three classes, whereas an attribute of the first class can be the type of any of the other classes:

#include<variant>   

class A {
   public:
   std::string to_string(){
       return var.to_string();
   }
   std::variant<B,C> var;
};

class D {
   virtual std::string to_string();
};

class B : public D {
   public:
   std::string to_string();
};

class C : public D {
   public:
   std::string to_string();
};

I want to invoke a method (lets say to_string) on the member of class A. I know that every variant has an implementation of the function.

The most effective I have come up with is from cppreference:

 if(const B* b = std::get_if<B>(&var))
          b->to_string(); 
 if(const C* c = std::get_if<C>(&var))
          c->to_string();

Especially when working with many variants, this seems ineffectiv and ugly and I am thinking that this should be possible with a one-liner. Is there a better way to do this?


Solution

  • As pointed out in the comments, since C and B inherit from D, you can just store a std::unique_ptr<D> (it has to be a pointer in order to get dynamic dispatch with object slicing). But if your real-world program has some constraints that prevent this from being viable, then your best friend when it comes to std::variant is std::visit.

    std::visit takes two arguments: a visitor and a variant. The visitor has to be an object of some form which is callable (i.e. has operator() defined) for each variant option (in your case, B and C). Then it calls the appropriate operator() for whatever is actually in the std::variant.

    We could make a new class that defines all of these callables for us. It'd be tedious.

    class MyCallable {
    
      std::string operator()(B& arg) {
        return arg.to_string();
      }
    
      std::string operator()(C& arg) {
        return arg.to_string();
      }
    
    };
    
    ...
    
    std::visit(MyCallable(), var)
    

    (Note: If you make to_string on D a const method then you can take a const T& rather than an lvalue reference, which is probably better here)

    But that's silly and incredibly verbose. We could also template it. That cuts down on the boilerplate a bit.

    class MyCallable {
    
      template <typename T>
      std::string operator()(T& arg) {
        return arg.to_string();
      }
    
    };
    
    ...
    
    std::visit(MyCallable(), var)
    

    That's a bit better, but it's silly that we have to define a whole class for this. Fortunately, modern C++ will let you define lambdas which have templated operator(), just like this but inline. So you can simply do this

    std::visit([](auto& x) { return x.to_string(); }, var)
    

    This will work for any std::variant, provided all variant options have a compatible to_string method (they don't even have to come from the same superclass; they can be unrelated to_string methods).