Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A std::visit-like function for visiting over polymorphic types

I've been recently trying C++17's std::variant and std::visit, and I found it quite powerful. I specially liked the ability to create a visitor pattern over several variant objects. Here is one example of what I mean:

std::variant<int, float, char> v1 { 's' };
std::variant<int, float, char> v2 { 10 };

std::visit(overloaded{
        [](int a, int b) { },
        [](int a, float b) { },
        [](int a, char b) { },
        [](float a, int b) { },
        [](auto a, auto b) { }, // << default!
    }, v1, v2);

See https://www.bfilipek.com/2018/09/visit-variants.html for full details. In view of this, I was wondering if it'd be possible to write similar code based on polymorphic types rather than variant objects.

Think about an scenario where we're using dynamic polymorphism and parent objects for writing a generic interface. Then we want to implement a certain functionality that depends on several polymorphic types, i.e. like

void fun(IFoo* ptr_foo, IBar* ptr_bar) {
  {
    Foo1* child_foo = dynamic_cast<Foo1*>(ptr_foo);
    Bar1* child_bar = dynamic_cast<Bar1*>(ptr_bar);
    if(child_foo && child_bar) { return fun(child_foo, child_bar) }
  }
  // ... other cases
  {
    Foo1* child_foo = dynamic_cast<Foo1*>(ptr_foo);
    BarN* child_bar = dynamic_cast<BarN*>(ptr_bar);
    if(child_foo && child_bar) { return fun(child_foo, child_bar) }
  }
  // ... other cases
  {
    FooN* child_foo = dynamic_cast<FooN*>(ptr_foo);
    BarN* child_bar = dynamic_cast<BarN*>(ptr_bar);
    if(child_foo && child_bar) { return fun(child_foo, child_bar) }
  }
  throw std::runtime_error{};
}

I know the above is far from optimal, but just trying to make the scenario as clear as possible.

In this scenario, using a virtual function for fun does not seem straightforward, as it depends on the type of two inputs. Besides, we're trying to avoid virtual methods for these functionalities, as we prefer keeping the interface for IFoo or IBar agnostic of these external functions.

Using a visitor pattern does not seem plausible either, for several input objects to the visiting function.

The simplest approach seems the example implementation using dynamic_cast I showed above, but this quickly escalates in the number of cases to write as we go from 1 to N inputs. The std::variant+std::visit approach above, though, covers this scenario so cleanly and straightforwardly.

So wrapping up, our constraints/requirements are:

  • A visitor pattern that takes several polymorphic objects as input
  • Non-critical runtime performance (so say using dynamic_cast is fine)
  • Having minimum boilerplate, akin to the std::visit approach

Is this possible somehow?

I was considering writing a variadic recursive templated function, akin to std::visit, which would automatically generate all type cases to check for. An example of use would be something like: visitPolymorphic<tuple<Foo1, Foo2>,tuple<Bar1, Bar2, Bar3>>(ptr_foo, ptr_bar) This would if-else over the different template input types, and dispatch the right call.

Any thoughts on this?

like image 443
jesusbriales Avatar asked Aug 21 '19 19:08

jesusbriales


People also ask

What is the difference between STD::variant and STD::visit?

Modern C++ Features - std::variant and std::visit - Simplify C++! std::variant is a library addition in C++17 for sum types, and std::visit is one of the ways to process the values in a std::variant.

What is a good example of a polymorphic variant?

Here's a basic example with one variant: We have a variant that represents a package with four various types, and then we use super-advanced VisitPackage structure to detect what's inside. The example is also quite interesting as you can invoke a polymorphic operation over a set of classes that are not sharing the same base type.

What is the difference between variant and visit in C++?

With std::variant we can express an object that might have many different types - like a type-safe union, all with value semantics. And then with std::visit we can call a visitor object that will invoke an operation based on the active type in the variant.

What is the use of visit function in C++?

std::visit is a powerful utility that allows you to call a function over a currently active type in std::variant. It does some magic to select the proper overload, and what's more, it can support many variants at once.


1 Answers

You can use std::variant there too:

struct Foo1;
struct Foo2;
struct Foo3;

using FooVariant = std::variant<Foo1*, Foo2*, Foo3*>;

struct IFoo
{
    virtual ~IFoo() = default;

    FooVariant AsVariant() = 0;

    // ...
};

struct Foo1 : IFoo
{
    FooVariant AsVariant() override { return this;}
    // ...
};
// Same for FooX

struct Bar1;
struct Bar2;
struct Bar3;

using BarVariant = std::variant<Bar1*, Bar2*, Bar3*>;

struct IBar
{
    virtual ~IBar() = default;

    BarVariant AsVariant() = 0;

    // ...
};

struct Bar1 : IBar
{
    BarVariant AsVariant() override { return this;}
    // ...
};
// Same for BarX

And then

void fun(IFoo& foo, IBar& bar) {
   std::visit(overloaded{
            [](Foo1* a, Bar1* b) { /*..*/ },
            [](Foo2* a, Bar2* b) { /*..*/ },
            [](Foo3* a, auto* b) { /*..*/ },
            [](auto* a, auto* b) { /*..*/ }, // << default!
        },
        foo.AsVariant(), bar.AsVariant()
    );
}

If you don't want that virtual AsVariant() in interface (but use dynamic_cast), you might still have free function:

FooVariant AsVariant(IFoo& foo)
{
    if (auto* p = dynamic_cast<Foo1*>(&foo)) {
        return p;
    }
    if (auto* p = dynamic_cast<Foo2*>(&foo)) {
        return p;
    }
    if (auto* p = dynamic_cast<Foo3*>(&foo)) {
        return p;
    }
    throw std::runtime_error("Invalid type");
}

like image 116
Jarod42 Avatar answered Oct 30 '22 12:10

Jarod42