Reputation: 413
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
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