Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why must std::visit have a single return type?

Tags:

c++

std

c++17

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.

like image 906
dtell Avatar asked May 08 '19 10:05

dtell


People also ask

What does visit() do c++?

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.

What does std :: visit do?

std::visitApplies the visitor vis (Callable that can be called with any combination of types from variants) to the variants vars .

Can std :: variant be empty?

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.


2 Answers

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.

like image 148
Max Langhof Avatar answered Oct 06 '22 01:10

Max Langhof


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.

like image 32
Lightness Races in Orbit Avatar answered Oct 05 '22 23:10

Lightness Races in Orbit