While playing around with std::variant
and std::visit
the following question came up:
Consider the following code:
using Variant = std::variant<int, float, double>;
auto lambda = [](auto&& variant) {
std::visit(
[](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int\n";
} else if (std::is_same_v<T, float>) {
std::cout << "float\n";
} else {
std::cout << "double\n";
}
},
variant);
};
It works fine as the following examples show:
lambda(Variant(4.5)); // double
lambda(Variant(4.f)); // float
lambda(Variant(4)); // int
Then why does the following fail:
using Variant = std::variant<int, float, double>;
auto lambda = [](auto&& variant) {
std::visit([](auto&& arg) { return arg; }, variant);
};
auto t = lambda(Variant(4.5));
due to the static assertion
static_assert failed due to requirement '__all<is_same_v<int
(*)(__value_visitor<(lambda at main.cc:25:7)> &&,
__base<std::__1::__variant_detail::_Trait::_TriviallyAvailable, int, float,
double> &), float (*)(__value_visitor<(lambda at main.cc:25:7)> &&,
__base<std::__1::__variant_detail::_Trait::_TriviallyAvailable, int, float,
double> &)>, is_same_v<int (*)(__value_visitor<(lambda at main.cc:25:7)> &&,
__base<std::__1::__variant_detail::_Trait::_TriviallyAvailable, int, float,
double> &), double (*)(__value_visitor<(lambda at main.cc:25:7)> &&,
__base<std::__1::__variant_detail::_Trait::_TriviallyAvailable, int, float,
double> &)> >::value' "`std::visit` requires the visitor to have a single
return type."
std::visit
can obviously deduce the type of arg
as the successful example shows. Then why the requirement to have a single return type?
Compiler is Apple LLVM version 10.0.1 (clang-1001.0.46.4)
but gcc version 8.3.0
fails with a similar error.
std::visit from C++17 is a powerful utility that allows you to call a function over a currently active type in std::variant . In this post, I'll show you how to leverage all capabilities of this handy function: the basics, applying on multiple variants, and passing additional parameters to the matching function.
std::visitApplies the visitor vis (Callable that can be called with any combination of types from variants) to the variants vars .
Empty variants are also ill-formed (std::variant<std::monostate> can be used instead). A variant is permitted to hold the same type more than once, and to hold differently cv-qualified versions of the same type.
The return type of std::visit
depends only on the types of the visitor and the variant passed to it. That's simply how the C++ type system works.
If you want std::visit
to return a value, that value needs to have a type at compile-time already, because all variables and expressions have a static type in C++.
The fact that you pass a Variant(4.5)
(so "clearly the visit would return a double") in that particular line doesn't allow the compiler to bend the rules of the type system - the std::visit
return type simply cannot change based on the variant value that you pass, and it's impossible to decide on exactly one return type only from the type of the visitor and the type of the variant. Everything else would have extremely weird consequences.
This wikipedia article actually discusses basically the exact situation/question you have, just with an if
instead of the more elaborate std::visit
version:
For example, consider a program containing the code:
if <complex test> then <do something> else <signal that there is a type error>
Even if the expression always evaluates to true at run-time, most type checkers will reject the program as ill-typed, because it is difficult (if not impossible) for a static analyzer to determine that the else branch will not be taken.
If you want the returned type to be "variant-ish", you have to stick with std::variant
. For example, you could still do:
auto rotateTypes = [](auto&& variant) {
return std::visit(
[](auto&& arg) -> std::variant<int, float, double> {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
return float(arg);
} else if (std::is_same_v<T, float>) {
return double(arg);
} else {
return int(arg);
}
},
variant);
};
The deduced return type of std::visit
then is std::variant<int, float, double>
- as long as you don't decide on one type, you must stay within a variant (or within separate template instantiations). You cannot "trick" C++ into giving up static typing with an identity-visitor on a variant.
Although each "implementation" is a different overload, and could thus have a different return type, at some point you will need a common point of access and that common point of access will need a single return type, because the selected variant type is only known at runtime.
It is common convention with a visitor to perform that logic inside the visit
code; indeed, the very purpose of std::visit
is to do all that magic for you and abstract away the runtime type switching.
Otherwise, you would basically be stuck reimplementing std::visit
at the callsite.
It's tempting to think that this could all be fixed using templates: after all, you've used generic lambdas so all these overloads are autonomously instantiated, so why can't the return type just be "known"? Again, it's only known at runtime, so that's no good to ya. There must be some static way of delivering the visitation result to the callsite.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With