tom
tom

Reputation: 413

return value from possible type in std::variant through std::visit

I am trying to wrap my head around std::variant and std::visit and I am trying to come up with a method to specify a couple of types that I would like my variable to hold (which would go into my std::variant) and then retrieve stored data through a std::visit. Consider the following example:

#include <iostream>
#include <variant>
#include <string>

struct PrintType {
  void operator()(const int &data) {
    std::cout << "visiting int node" << std::endl;
  }
  void operator()(const double &data) {
    std::cout << "visiting double node" << std::endl;
  }
};

struct SingleOperatorOverload {
  int operator()(const int &data) {
    std::cout << "visiting int node" << std::endl;
    return data;
  }
};

struct AllTypesOperatorOverload {
  int operator()(const int &data) {
    std::cout << "visiting int node" << std::endl;
    return data;
  }
  double operator()(const double &data) {
    std::cout << "visiting double node" << std::endl;
    return data;
  }
};

int main() {

  using var_t = std::variant<int, double>;

  // print int related operator() content, OK
  var_t foo = 42;
  std::visit(PrintType(), foo);

  // print double related operator() content, OK
  foo = 3.1415;
  std::visit(PrintType(), foo);

  // get value and store into bar, struct with single operator(), OK
  foo = 42;
  auto bar = std::visit(SingleOperatorOverload(), foo);
  std::cout << "bar: " << bar << std::endl;

  // get value and store into bar, struct with multiple operator(), ERROR
  auto bar = std::visit(AllTypesOperatorOverload(), foo);
  std::cout << "bar: " << bar << std::endl;

  return 0;
}

The variant is allowed to hold (in this simplified example) either int or double. If I just want to print something based on the type (as done with the PrintType struct), that works fine.

If I want to retrieve data through a visitor as done in the SingleOperatorOverload class, which only provides an implementation for the operator() accepting an int as a parameter, that works. However, as soon as I try to implement an operator() for each type in the std::variant, i.e. here int and double, as in the AllTypesOperatorOverload struct, I get a compilation error error: invalid conversion from '...' {aka double ...} to '...' {aka int ...} so it seems std::variant is handling function signatures differently?

I tried SFINAE but that does not seem to alleviate the problem

struct AllTypesOperatorOverload {
  template<typename T, std::enable_if_t<std::is_same<T, int>::value>>
  T operator()(const T &data) {
    std::cout << "visiting int node" << std::endl;
    return data;
  }
  template<typename T, std::enable_if_t<std::is_same<T, double>::value>>
  T operator()(const T &data) {
    std::cout << "visiting double node" << std::endl;
    return data;
  }
};

This will now report an error: no type named 'type' in 'struct std::invoke_result<AllTypesOperatorOverload, int&>'. Is there a way to provide operator() for all types and then receive their respective values into bar with the correct type depending on how foo has been set? I am aware of std::get_if<T>() which may be of use here, but ideally, I don't want to have a long if statement checking for each type, unless absolutely necessary (this is a simplified example, I may want to have several more types in my std::variant).

Upvotes: 4

Views: 2642

Answers (1)

Justin
Justin

Reputation: 25277

The error messages suck, but the issue here is that all of the alternatives of the variant must have the same return type in the visitor. Your AllTypesOperatorOverload does not obey this rule, returning a double and an int, which are not the same type.

The newest versions of libstdc++ or any version of libc++ produce much better error messages that explicitly tell you this (the following is word wrapped by me):

error: static_assert failed due to requirement '__visit_rettypes_match'
    "std::visit requires the visitor to have the same return type for
     all alternatives of a variant"
              static_assert(__visit_rettypes_match,

This makes sense, because when you look at this line, what is the type of bar?

auto bar = std::visit(AllTypesOperatorOverload(), foo);

If you were allowed to return differing types, bar's type would depend on which alternative foo holds at runtime. That can't work in C++.


Note that there are easier ways to create visitors for std::visit that use lambdas instead of externally defined structs. You can use if constexpr:

std::visit([](auto value) {
    if constexpr (std::is_same_v<int, decltype(value)>) {
        std::cout << "visiting int\n";
    } else {
        static_assert(std::is_same_v<double, decltype(value)>);
        std::cout << "visiting double\n";
    }
    std::cout << "bar: " << value << '\n';
}, foo);

Or alternatively, you can define an overloaded helper struct that lets you overload lambdas:

template <typename... Lambdas>
struct overloaded : Lambdas...
{
    template <typename... Fns>
    explicit constexpr overloaded(Fns&&... fns)
        : Lambdas(std::forward<Fns>(fns))...
    {}

    using Lambdas::operator()...;
};
template <typename... Lambdas>
overloaded(Lambdas...) -> overloaded<Lambdas...>;

// Usage:
std::visit(overloaded{
    [](int value) {
        std::cout << "visiting int\n";
        std::cout << "bar: " << value << '\n';
    },
    [](double value) {
        std::cout << "visiting double\n";
        std::cout << "bar: " << value << '\n';
    }
}, foo);

Upvotes: 10

Related Questions