Leonardo Alt
Leonardo Alt

Reputation: 325

C++17 Infer return type of lambda based visitor

Here's the lambda overload based visitor given in https://en.cppreference.com/w/cpp/utility/variant/visit:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
std::visit(overloaded {
  [](auto arg) { std::cout << arg << ' '; },
  [](double arg) { std::cout << std::fixed << arg << ' '; },
  [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
}, v);

where v is a variant and all its types must have implemented lambdas.

I want to add a function to overloaded that has the same return type as all lambdas (which is then the return type of visit(overloaded{...}, v)). Something similar to:

template<class... Ts> struct overloaded : Ts...
{
  using Ts::operator()...;

  common_type_t<decltype(declval<Ts>())...> test() const { return {}; }
};
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
auto r = std::visit(overloaded {
  [](auto arg) { return 1; },
  [](double arg) { return 2; },
  [](const std::string& arg) { return 3; },
}, v);

Here something goes wrong and common_type<_Tp...>::type doesn't exist. I thought about using common_type_t<invoke_result_t<Ts>...> but I don't have the lambdas' arguments, so invoke_result<Ts..>>::type fails there.

What am I missing?

Upvotes: 2

Views: 888

Answers (1)

Evg
Evg

Reputation: 26342

This problem has no general solution, because if one of the lambdas is generic, e.g.

[](auto arg) { return arg; }

then there is no way to deduce the return type without std::variant object.

However, we can try to deduce common type from those lambdas that have a non-template operator(). The solution for this special case follows.

First make a trait that deduces the return type of a lambda, or returns empty if deduction fails:

struct empty {};

template<typename Ret, typename Fn, typename Arg>
Ret return_type(Ret(Fn::*)(Arg));

template<typename Ret, typename Fn, typename Arg>
Ret return_type(Ret(Fn::*)(Arg) const);

template<typename Fn>
auto return_type(Fn) -> decltype(return_type(&Fn::operator()));

empty return_type(...);

Then tweak std::common_type:

template<>
struct std::common_type<empty, empty> {
    using type = empty;
};

template<typename T>
struct std::common_type<T, empty> {
    using type = T;
};

template<typename T>
struct std::common_type<empty, T> {
    using type = T;
};

And finally put all this together:

template<class... Ts> struct overloaded : Ts... {
    using Ts::operator()...; 
    static auto test() ->
        std::common_type_t<decltype(return_type(std::declval<Ts>()))...>;
};

Example:

auto fn = overloaded {
    [](auto arg)    { },
    [](int arg)     { return 0;  },
    [](std::string) { return 1.; }
};

static_assert(std::is_same_v<decltype(fn.test()), double>);

If no type can be deduced, the return type of test() will be empty:

auto fn = overloaded {
    [](auto arg) { }
};

static_assert(std::is_same_v<decltype(fn.test()), empty>);

Upvotes: 2

Related Questions