c++variantc++23

how to transform std::variant by applying specified function


I need to convert variant<type1, type2, ...> to another variant<ftype1, ftype2, ...> where ftypeN is the return type of function fn() invoked with parameter of typeN, subject to the following conditions:

The resultant variant should have the same index as input variant and value equal to fn(get<I>(v)) or default initialized void_type or wrong_arg_type. Function fn() should be called at most once.

This should work with three current C++23 compilers: gcc/clang/MSVC

This is my current implementation of return type deduction:

struct void_type{};
struct wrong_arg_type{};

template<class Fn, class...Args>
using ret_t = std::conditional_t<std::is_invocable_v<Fn, Args...>,
    std::conditional_t<std::is_void_v<std::invoke_result_t<Fn, Args...>>, 
        void_type,
        std::invoke_result_t<Fn, Args...>>,
    wrong_arg_type
    >;

template<class Fn, class... Args>
constexpr auto invoke_fn(Fn fn, Args&&...args)
{
    if constexpr(std::is_same_v<ret_t<Fn, Args...>, void_type>)
        return std::invoke(std::forward<Fn>(fn), std::forward<Args>(args)...), 
            void_type{};
    else if constexpr(std::is_same_v<ret_t<Fn, Args...>, wrong_arg_type>)
        return wrong_arg_type{};
    else
        return std::invoke(fn, std::forward<Args>(args)...);
}

And my current implementation of transform function:

template<class Fn, class...T>
auto transform(Fn fn, const std::variant<T...>& v)
{
    using variant_type = std::variant<ret_t<Fn, T>...>;

    return [&]<auto N>(this auto&& self, std::size_t idx, std::index_sequence<N>)
    -> variant_type
    {
        if constexpr(N == sizeof...(T))
            { throw "transform error"; }
        else if (N == idx)
            return variant_type(std::in_place_index<N>, invoke_fn(fn, std::get<N>(v)));
        else
            return self(idx, std::index_sequence<N + 1>{});
    }(v.index(), std::index_sequence<0>{});
}

Compiler explorer demo is here.

There are two problems with this code I currently can't figure out.

First, the case when function is not invocable fails to compile (std::tuple in the above demo).

<source>:15:29: error: no type named 'type' in 'struct std::invoke_result<overloaded<main()::<lambda(double)>, main()::<lambda(int)>, main()::<lambda(const std::string&)> >, std::tuple<int, int> >'
   15 |     std::conditional_t<std::is_void_v<std::invoke_result_t<Fn, Args...>>,
      |                        ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Second, MSVC gives some cryptic error even when the function is invocable.

<source>(44): error C2231: '.fn': left operand points to 'class', use '->'

Could you suggest a fix for those issues?


Solution

  • I found a solution to the problem of std::conditional by changing the implementation of invoke_fn, due to the fact that only one if constexpr branch being evaluated, and ret_t is just a decltype of it.

    template<class Fn, class... Args>
    constexpr auto invoke_fn(Fn fn, Args&&...args)
    {
        if constexpr(not std::is_invocable_v<Fn, Args...>)
            return wrong_arg_type{};
        else if constexpr(std::is_void_v<std::invoke_result_t<Fn, Args...>>)
            return std::invoke(std::forward<Fn>(fn), std::forward<Args>(args)...), 
                void_type{};
        else
            return std::invoke(fn, std::forward<Args>(args)...);
    }
    
    template<class Fn, class... Args>
    using ret_t = decltype(invoke_fn(std::declval<Fn>(), std::declval<Args>()...));
    

    The second problem is related to MSVC buggy implementation of recursive lambda. Replacing it with a template function solves this problem.

    template<auto N, class Fn, class...T>
    auto transform_helper(Fn fn, const std::variant<T...>& v)
    ->std::variant<ret_t<Fn, T>...>
    {
        using variant_type = std::variant<ret_t<Fn, T>...>;
    
        if constexpr(N == sizeof...(T))
            { throw "transform error"; }
        else if (N == v.index())
            return variant_type(std::in_place_index<N>, invoke_fn(fn, std::get<N>(v)));
        else
            return transform_helper<N+1>(fn, v);
    }
    
    template<class Fn, class...T>
    auto transform(Fn fn, const std::variant<T...>& v)
    {
        return transform_helper<0>(fn, v);
    }
    

    Here is a working demo