Variant visitation and common_type
Asked Answered
D

4

7

I am wondering how std::visit return type conversions are supposed to work.

The context is the following: I have a variant object and I want to apply (through std::visit) different functions depending on its underlying type. The result of each function may have a different type, but then I would like std::visit to pack it up in a variant type.

Pseudo-code:

I have:

variant<A,B> obj
f(A) -> A
f(B) -> B

I want:

if obj is of type A => apply f(A) => resA of type A => pack it in variant<A,B>
if obj is of type B => apply f(B) => resB of type B => pack it in variant<A,B>

Now, according to cppreference, the return type of std::visit is "The value returned by the selected invocation of the visitor, converted to the common type of all possible std::invoke expressions" But what common type means is not specified. Is it std::common_type ? In this case, it doesn't work with gcc 7.2:

#include <variant>
#include <iostream>
#include <type_traits>

struct A {
    int i;
};
struct B {
    int j;
};

// the standard allows to specialize std::common_type
namespace std {
    template<>
    struct common_type<A,B> {
        using type = std::variant<A,B>;
    };
    template<>
    struct common_type<B,A> {
        using type = std::variant<A,B>;
    };
}


struct Functor {
    auto
    operator()(A a) -> A {
        return {2*a.i};
    }
    auto
    operator()(B b) -> B {
        return {3*b.j};
    }
};


int main() {
    std::variant<A,B> var = A{42};

    auto res = std::visit( Functor() , var ); // error: invalid conversion from 'std::__success_type<B>::type (*)(Functor&&, std::variant<A, B>&) {aka B (*)(Functor&&, std::variant<A, B>&)}' to 'A (*)(Functor&&, std::variant<A, B>&)' [-fpermissive]

}

What should I do to express this unpack - apply visitation - repack pattern?

Notes:

1) Specializing std::common_type<A(*)(Ts...),B(*)(Ts...)> won't cut it. This would do the trick but rely on a particular std::lib implementation detail. Plus it doesn't work for multi-visitation.

2) The example I have given is really reduced to the bare minimum, but you have to imagine that the visitation mechanism I want to provide is on the library side, and the visitors are on the client side and can be arbitrary complicated: unknown number and types of arguments, unknown return types. The library should just provide visitation and a pre-defined set of std::common_type specializations to be used for visitation return types. So for instance, defining

auto f = [](auto x) -> variant<A,B> { return Functor()(x); };

and then applying std::visit to f is not a viable option: from the library side, I can't predefine this kind of lambda without knowing the "packed" return type. [The main problem is that I see no way of asking the language for the std::common_type of a particular overload set]

Dredge answered 7/9, 2017 at 19:26 Comment(1)
@Jarod42 Yes you are right, this is not the problem. I wasn't clear so I edited the question: the real missing part is really the overload set common return typeBorneo
R
5

You can create your own visit layer, something like:

template <typename Visitor, typename ... Ts>
decltype(auto) my_visit(Visitor&& vis, const std::variant<Ts...>& var)
{
    return std::visit([&](auto&& e)
        -> std::common_type_t<decltype(vis(std::declval<Ts>()))...>
        {
            return vis(e);
        }, var);
}

Demo

Rewarding answered 7/9, 2017 at 20:6 Comment(7)
Hum interesting, I didn't think about that. It works for single visitation, but I don't see right now how to extend it to multiple visitation.Borneo
To expand to multiple, you also have to have common_type for variant<A, B> and C which is indeed not really scalable. Yakk's Solution is more scalable, but you have still some work(remove duplicated type,...).Rewarding
I am working on generating the cross product of argument overload set and it's quite complex... Once I am done, extending your solution is trivial. I'll post it here.Borneo
I don't understand Yakk's solution, can you explain it ?Borneo
invoke_result_t<F, Fs>... is the equivalent of decltype(vis(std::declval<Ts>())).... Instead of using common_type, he wraps it in a container (as tuple) (so obtains something like types<A, B, C, A>). Then you have to remove duplicate, and transform to the wanted type. types<int, char> can give variant<int, char> or int.Rewarding
Ok this makes sense. But I think you don't really need to remove the duplicate since the specialization of types<A,B,C,A> is (or should be required to be) a kind of accumulator over an equivalence relation types<X,Y> (i.e. reflexive, transitive, symmetric) which should behave like types<A,B,C,A>==types<A,A,B,C>==types<A,B,C> (std::common_type satisfies this requirements by default)Borneo
As wrote, types doesn't satisfy that (variant neither). And that requirement is the additional stuff which have indeed several possible ways/implementations.Rewarding
F
3
template<class...>struct types{using type=types;};

template<class F, class Types>
struct results;

template<class F, class...Ts>
struct results<F, types<Ts...>>:
  types<std::invoke_result_t<F,Ts>...>
{};

this gives you a the result of applying F to a bundle of types as a bundle of types.

Add transcribe to-from variant, maybe duplicate removal, a wrapper that takes F and a variant<Ts...> and creates an F2 that calls F and returns said variant, then passes F2 to visit, and we are hakf way there.

The other half is to handle multiple variants. To get that, we need to take cross product of multiple type bundles, get the invoke result of all of them, and bundle that up.

Farreaching answered 7/9, 2017 at 20:9 Comment(0)
W
2

Your main problem is the fact that std::visit expressly requires all return types of the various invocations provided by the Visitor to be of the same type, and specializing std::common_type does nothing to fix that. The "Common Type" descriptor you pulled from the Standard is meant colloquially, not as a literal type.

In other words, the Visitor must take the form of

struct Visitor {
    using some_type = /*...*/;

    some_type operator()(A const& a);
    some_type operator()(B const& b);

};

Fortunately, this is a problem that solves itself. Because there already is a common type that can be assigned from this sort of permutation on the stored value: the variant you described in the first place.

struct Functor {
    std::variant<A,B> operator()(A const& a) const {
        return A{2*a.i};
    }
    std::variant<A,B> operator()(B const& b) const {
        return B{3*b.j};
    }
};

This should compile and yield the behavior you're expecting.

Weald answered 7/9, 2017 at 20:8 Comment(3)
@Rewarding Fixed. I didn't catch that.Weald
However, the problem mentioned in note #2 still remains: in the general case, I don't want my client to return the variant, and I can't wrap their functions because I don't know the return typesBorneo
@Dredge You need to be more specific regarding why you don't want to return a variant object. Because the solution to your problem differs depending on the precise reasoning that an API returning variant<...> is bad for you.Weald
D
0

My solution for multiple visitation. Thanks to Jarod42 for showing me the way with single variant visitation.

Live Demo

The main problem is to generate the cross-product of all possible calls to an overload set. This answer does not address the problem of a generic conversion of return types, I just did an ad-hoc specialization of std::common_type (I think this is enougth to suit my needs, but feel free to contribute!).

See compile-time tests at the end to understand each template meta-function.

Feel free to suggest simplifications (std::index_sequence anyone?)

#include <variant>
#include <iostream>
#include <type_traits>

// ========= Library code ========= //

// --- Operations on types --- //
template<class... Ts>
struct Types; // used to "box" types together



// Lisp-like terminology 
template<class Head, class Tail>
struct Cons_types;

template<class Head, class... Ts>
struct Cons_types<Head,Types<Ts...>> {
    using type = Types<Head,Ts...>;
};




template<class... _Types>
struct Cat_types;

template<class _Types, class... Other_types>
struct Cat_types<_Types,Other_types...> {
    using type = typename Cat_types<_Types, typename Cat_types<Other_types...>::type>::type;
};

template<class... T0s, class... T1s>
struct Cat_types< Types<T0s...> , Types<T1s...> > {
    using type = Types< T0s..., T1s... >;
};
template<class... T0s>
struct Cat_types< Types<T0s...> > {
    using type = Types< T0s... >;
};




template<class Head, class Types_of_types>
struct Cons_each_types;

template<class Head, class... Ts>
struct Cons_each_types<Head,Types<Ts...>> {
    using type = Types< typename Cons_types<Head,Ts>::type... >;
};
template<class Head>
struct Cons_each_types<Head,Types<>> {
    using type = Types< Types<Head> >;
};




template<class _Types>
struct Cross_product;

template<class... Ts, class... Other_types>
struct Cross_product< Types< Types<Ts...>, Other_types... > > {
    using type = typename Cat_types< typename Cons_each_types<Ts,typename Cross_product<Types<Other_types...>>::type>::type...>::type;
};

template<>
struct Cross_product<Types<>> {
    using type = Types<>;
};





// --- Operations on return types --- //
template<class Func, class _Types>
struct Common_return_type;

template<class Func, class... Args0, class... Other_types>
struct Common_return_type<Func, Types< Types<Args0...>, Other_types... >> {

    using type =
        std::common_type_t< 
            std::result_of_t<Func(Args0...)>, // C++14, to be replaced by std::invoke_result_t in C++17
            typename Common_return_type<Func,Types<Other_types...>>::type
        >;
};

template<class Func, class... Args0>
struct Common_return_type<Func, Types< Types<Args0...> >> {
    using type = std::result_of_t<Func(Args0...)>;
};




// --- Operations on variants --- //
template<class... Vars>
struct Vars_to_types;

template<class... Ts, class... Vars>
struct Vars_to_types<std::variant<Ts...>,Vars...> {
    using type = typename Cons_types< Types<Ts...> , typename Vars_to_types<Vars...>::type >::type;
};

template<>
struct Vars_to_types<> {
    using type = Types<>;
};




template<class Func, class... Vars>
// requires Func is callable
// requires Args are std::variants
struct Common_return_type_of_variant_args {
    using Variant_args_types = typename Vars_to_types<Vars...>::type;

    using All_args_possibilities = typename Cross_product<Variant_args_types>::type;

    using type = typename Common_return_type<Func,All_args_possibilities>::type;
};




template <typename Func, class... Args>
// requires Args are std::variants
decltype(auto)
visit_ext(Func&& f, Args... args) {

    using Res_type = typename Common_return_type_of_variant_args<Func,Args...>::type;
    return std::visit(
        [&](auto&&... e)
        -> Res_type
        {
            return f(std::forward<decltype(e)>(e)...);
        },
        std::forward<Args>(args)...);
}








// ========= Application code ========= //

struct A {
    int i;
};
struct B {
    int j;
};


// This part is not generic but is enough
namespace std {
    template<>
    struct common_type<A,B> {
        using type = std::variant<A,B>;
    };
    template<>
    struct common_type<B,A> {
        using type = std::variant<A,B>;
    };

    template<>
    struct common_type<A,std::variant<A,B>> {
        using type = std::variant<A,B>;
    };
    template<>
    struct common_type<std::variant<A,B>,A> {
        using type = std::variant<A,B>;
    };

    template<>
    struct common_type<B,std::variant<A,B>> {
        using type = std::variant<A,B>;
    };
    template<>
    struct common_type<std::variant<A,B>,B> {
        using type = std::variant<A,B>;
    };
}


struct Functor {
    auto
    operator()(A a0,A a1) -> A {
        return {a0.i+2*a1.i};
    }
    auto
    operator()(A a0,B b1) -> A {
        return {3*a0.i+4*b1.j};
    }
    auto
    operator()(B b0,A a1) -> B {
        return {5*b0.j+6*a1.i};
    }
    auto
    operator()(B b0,B b1) -> B {
        return {7*b0.j+8*b1.j};
    }
};




// ========= Tests and final visit call ========= //
int main() {

    std::variant<A,B> var0;
    std::variant<A,B> var1;

    using Variant_args_types = typename Vars_to_types<decltype(var0),decltype(var1)>::type;
    static_assert(
        std::is_same_v<
            Types< Types<A,B>, Types<A,B> >,
            Variant_args_types
        >
    );


    using Cons_A_Nothing  = typename Cons_each_types<A, Types<> >::type;
    static_assert(
        std::is_same_v<
            Types< Types<A> >,
            Cons_A_Nothing
        >
    );

    using Cons_A_AB = typename Cons_each_types<A, Types<Types<A>,Types<B>> >::type;
    using Cons_B_AB = typename Cons_each_types<B, Types<Types<A>,Types<B>> >::type;
    static_assert(
        std::is_same_v<
            Types< Types<A,A>, Types<A,B> >,
            Cons_A_AB
        >
    );

    using Cat_types_A = typename Cat_types<Cons_A_Nothing>::type;
    static_assert(
        std::is_same_v<
            Types< Types<A> >,
            Cat_types_A
        >
    );
    using Cat_types_AA_AB_BA_BB = typename Cat_types<Cons_A_AB,Cons_B_AB>::type;
    static_assert(
        std::is_same_v<
            Types< Types<A,A>, Types<A,B>, Types<B,A>, Types<B,B> >,
            Cat_types_AA_AB_BA_BB
        >
    );


    using Depth_x1_1_cross_product =  typename Cross_product<Types<Types<A>>>::type;
    static_assert(
        std::is_same_v<
            Types< Types<A> >,
            Depth_x1_1_cross_product
        >
    );


    using Depth_x2_1_1_cross_product =  typename Cross_product<Types<Types<A>,Types<B>>>::type;
    static_assert(
        std::is_same_v<
            Types< Types<A,B> >,
            Depth_x2_1_1_cross_product
        >
    );

    using All_args_possibilities = typename Cross_product<Variant_args_types>::type;
    static_assert(
        std::is_same_v<
            Types< Types<A,A>, Types<A,B>, Types<B,A>, Types<B,B> >,
            All_args_possibilities
        >
    );

    using Functor_AorB_AorB_common_return_type = typename Common_return_type<Functor,All_args_possibilities>::type;
    static_assert(
        std::is_same_v<
            std::variant<A,B>,
            Functor_AorB_AorB_common_return_type
        >
    );

    using Functor_varAB_varAB_common_return_type = typename Common_return_type_of_variant_args<Functor,decltype(var0),decltype(var1)>::type;
    static_assert(
        std::is_same_v<
            std::variant<A,B>,
            Functor_varAB_varAB_common_return_type
        >
    );


    var0 = A{42};
    var1 = A{43};
    auto res0 = visit_ext(Functor(), var0,var1);
    std::cout << "res0 = " << std::get<A>(res0).i << "\n";

    var0 = A{42};
    var1 = B{43};
    auto res1 = visit_ext(Functor(), var0,var1);
    std::cout << "res1 = " << std::get<A>(res1).i << "\n";


    var0 = B{42};
    var1 = A{43};
    auto res2 = visit_ext(Functor(), var0,var1);
    std::cout << "res2 = " << std::get<B>(res2).j << "\n";


    var0 = B{42};
    var1 = B{43};
    auto res3 = visit_ext(Functor(), var0,var1);
    std::cout << "res3 = " << std::get<B>(res3).j << "\n";
}
Dredge answered 8/9, 2017 at 11:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.