user1798707
user1798707

Reputation: 361

Use std::variant as class member and apply visitor

I'm trying to use std::variant as a class member variable and then use operator overloading so that the two Variants of this class can use the operator plus to produce a new variable. The problem is that std::get does not work as I thought and so I cannot retrieve the correct (hardcoded) string types so that the AddVisitor struct is used.

I get a compilation error that says: no matching function for call to ‘get<0>(std::basic_string&)’

Also is there a way that operator+ function detects the type without if-else statements?

I have already checked a lot of answers in SO including ones that answer questions about similar Boost functionality, but I cannot get it to work.

#include <iostream>
#include <variant>
#include <string>
#include "stdafx.h"


using Variant = std::variant<int, std::string>;

template<typename T>
struct AddVisitor
{
    T operator()(T v1, T v2)
    {
        return v1 + v2;
    }
};

class Var
{
    Variant v;

public:

    template<typename T>
    Var(T value) : v(value) {}

    Var operator+(Var& val)
    {
        // PROBLEM: This is a hard coded example that I want to use, so that concatenation of two strings happens.
        return std::visit(AddVisitor<std::string>(), std::get<std::string>(v), std::get<std::string>(val.get()));
       // Is there a way to get the correct type without if-else statements here?
    }

   Variant get()
   {
     return v;
   }
};

int main()
{
    Var x("Hello "), y("World");

    // The expected output is this:
    Var res = x + y;

    return 0;
}

I expect to be able to use the plus operator and concatenate two strings or two integers and create a new Var variable.

Upvotes: 2

Views: 3012

Answers (1)

bolov
bolov

Reputation: 75815

Ok, so there are a few things to talk about.

First, the visitor for std::visit with more than one variant argument should accept all combinations of variant types. In your case it should accept:

  • (string, string)
  • (string, int)
  • (int, int)
  • (int, string)

If for you only string, string and int, int are valid you still need to accept the other combinations for the code to compile, but you can throw in them.

Next, the visitor shouldn't be templated. Instead the operator() should be templated or overloaded for all the above combinations.

So here is AddVisitor:

struct AddVisitor
{
    auto operator()(const std::string& a, const std::string& b) const -> Variant
    {
        return a + b;
    }
    auto operator()(int a, int b) const -> Variant
    {
        return a + b;
    }

    // all other overloads invalid
    template <class T, class U>
    auto operator()(T, U) const -> Variant
    {
        throw std::invalid_argument{"invalid"};
    }
};

It's not clear from documentation what the overloads can return, but I couldn't make it compile unless all return Variant. Fortunately the compiler errors are TREMENDOUSLY HELPFULL . (I need to check the standard).

Next, when you call std::visit you need to pass the variants you have.

So the final code is this:

auto operator+(Var& val) -> Var
{
    return std::visit(AddVisitor{}, get(), val.get());
}

And you can indeed use it like you want:

Var res = x + y;

Another issue with your code is that get makes unnecessary copies. And copies of std::variant are not cheap to make. So I suggest:

auto get() const -> const Variant&  { return v; }
auto get() -> Variant&  { return v; }

Upvotes: 5

Related Questions