Search code examples
c++templatesvariadic-templatesstd-variant

How do you write a function template that determines if two arbitrary variants are holding the same type?


Consider the following function for determining if variables of identical variant types are holding the same type:

#include <variant>
#include <iostream>

template <typename... Ts>
bool hold_same_types(const std::variant<Ts...>& v1, const std::variant<Ts...>& v2) {
    return ((std::holds_alternative<Ts>(v1) && std::holds_alternative<Ts>(v2)) || ...);
}

struct foo {};
struct bar {};
struct quux {};
struct mumble {};

using var1 = std::variant<foo, bar, quux>;

int main()
{
    var1 b1 = bar{};
    var1 b2 = bar{};
    var1 q = quux{};

    std::cout << "(b1 , b2)  => " << hold_same_types(b1, b2) << "\n";
    std::cout << "(b1 , q)  => " << hold_same_types(b1, q) << "\n";
}

There seems to be no easy way to extend hold_same_types to handle heterogenous variants. For example, the following doesn't work

//...
using var1 = std::variant<foo, bar, quux>;
using var2 = std::variant<bar, quux, mumble>;

template <typename... Ts, typename... Us>
bool hold_same_types(const std::variant<Ts...>& v1, const std::variant<Us...>& v2) {
    return ((std::holds_alternative<Ts>(v1) && std::holds_alternative<Ts>(v2)) || ...);
}

int main()
{
    var1 b1 = bar{};
    var2 m = mumble{};

    std::cout << "(b1 , m)  => " << hold_same_types(b1, m) << "\n";
}

because

  1. You can't use multiple parameter packs like that.

  2. Even if you could get around that, the fold expression will not compile if Ts... contains a type that is not in Us....

That is, std::holds_alternative<T>(some_variant) will not return false if T is not an alternative in some_variant, it won't even compile.

I've tried some more complicated approaches, but I couldn't get them to work. The main problem I run into is generally not being able to use two parameter packs.


Solution

    1. You can change the template parameters of your hold_same_types to template <typename Variant, typename... Ts>, because it suffices to check all types of one variant type with the other variant. If the other variant does not have the type, it will definitely not old the same type.
    2. You could write your own my_holds_alternative implementation, that returns false if the type is not included in the variant, instead of a failing static_assert.
    #include <variant>
    #include <iostream>
    
    template <typename Variant, typename T, size_t... I>
    constexpr bool has_type_impl(std::index_sequence<I...>)
    {
        return (std::is_same_v<std::variant_alternative_t<I, Variant>, T> || ...);
    }
    
    template <typename Variant, typename T>
    constexpr bool has_type()
    {
        return has_type_impl<Variant, T>(std::make_index_sequence<std::variant_size_v<Variant>>());
    }
    
    template <typename T, typename Variant>
    constexpr bool my_holds_alternative(Variant const& v)
    {
        if constexpr (has_type<Variant, T>())
        {
            return std::holds_alternative<T>(v);
        } else {
            return false;
        }
    }
    
    template <typename Variant, typename... Ts>
    constexpr bool hold_same_types(const Variant& v1, const std::variant<Ts...>& v2)
    {
        return ((my_holds_alternative<Ts>(v1) && my_holds_alternative<Ts>(v2)) || ...);
    }
    
    struct foo {};
    struct bar {};
    struct quux {};
    struct mumble {};
    
    using var1 = std::variant<foo, bar, quux>;
    using var2 = std::variant<bar, quux, mumble>;
    
    int main()
    {
        var1 b1 = bar{};
        var1 b2 = bar{};
        var1 q = quux{};
    
        std::cout << "(b1 , b2)  => " << hold_same_types(b1, b2) << "\n";
        std::cout << "(b1 , q)  => " << hold_same_types(b1, q) << "\n";
    
        var2 m = mumble{};
    
        std::cout << "(b1 , m)  => " << hold_same_types(b1, m) << "\n";
    }
    
    (b1 , b2)  => 1
    (b1 , q)  => 0
    (b1 , m)  => 0
    

    https://godbolt.org/z/Ej1adsfqM