user1934513
user1934513

Reputation: 725

Is it ok to use std::variant of std::variants

I'm trying to combine two variants into one variant just for readability. This is the code:

using VariantType_basic = std::variant<int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, std::string>;
using VariantType_vector = std::variant<vector<int8_t>, vector<uint8_t>, vector<int16_t>, vector<uint16_t>, vector<int32_t>, vector<uint32_t>, vector<int64_t>, vector<uint64_t>, vector<std::string>>;
using VariantType_all = std::variant<VariantType_basic, VariantType_vector>;

class Container {
    public:
        Container(){}
        template<typename T>
        T get(string key, bool &found){
            found = false;
            T result;
            auto elem = m_internal_map.find(key);
            if(elem != m_internal_map.end())
                std::visit( 
                    [&](const auto& v){ 
                        // if(holds_alternative<T>(v)){

                        result = std::get<T>(v);
                        found = true;
                    //} 
                },  
                    elem->second 
                    );

            return result;
        }

        template<typename T>
        void put(string key, T&elem){

        }
    private:
        map<string, VariantType_all> m_internal_map;
};

The get() method fails at compile time at result = std::get<T>(v); when I try to do something like this:

Container cont;
bool found;
cont.get<uint16_t>("www", found);

The error message is huge but the first error message is this: /usr/include/c++/7/variant:762:7: error: static assertion failed: T should occur for exactly once in alternatives

Should I stop trying to use variant of variants ?

Upvotes: 0

Views: 2544

Answers (3)

Jarod42
Jarod42

Reputation: 217085

I suggest to "flatten" the variants instead of having variant of variants:

template <typename Var1, typename Var2> struct variant_flat;

template <typename ... Ts1, typename ... Ts2>
struct variant_flat<std::variant<Ts1...>, std::variant<Ts2...>>
{
    using type = std::variant<Ts1..., Ts2...>;
};

using VariantType_basic = std::variant<int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, std::string>;
using VariantType_vector = std::variant<std::vector<int8_t>, std::vector<uint8_t>, std::vector<int16_t>, std::vector<uint16_t>, std::vector<int32_t>, std::vector<uint32_t>, std::vector<int64_t>, std::vector<uint64_t>, std::vector<std::string>>;
using VariantType_all = variant_flat<VariantType_basic, VariantType_vector>::type;

And than your class has just to handle one variant level:

class Container
{
public:
    Container(){}

    template<typename T>
    T get(const std::string& key, bool &found) const {
        found = false;
        T result;
        auto elem = m_internal_map.find(key);
        if (elem != m_internal_map.end() && std::holds_alternative<T>(elem->second)){
            result = std::get<T>(elem->second);
            found = true;
        }
        return result;
    }

    template<typename T>
    void put(const std::string& key, const T& elem) {
        m_internal_map[key] = elem;
    }
private:
    std::map<std::string, VariantType_all> m_internal_map;
};

Demo

Upvotes: 7

Michael Kenzel
Michael Kenzel

Reputation: 15943

The problem is that std::visit will instantiate calls to your lambda for all variant types at compiletime and then just picks the one corresponding to the actual value contained within your variant at runtime. With the way the lambda is currently written, this means that it'll try to instantiate

result = std::get<T>(v);

for your given T with both, a version of the lambda for the case of VariantType_basic as well as for the case of VariantType_vector. Since your VariantType_vector does not at all contain a std::uint16_t, this second instantiation will fail to compile with the given error since you're attempting to call std::get<T> on a type that does not contain T in the list of alternatives at all…

One way to solve this problem would be to write your visitor lambda such that the code that calls std::get<T> is only instantiated for variant values that are variants that actually may contain a value of the type you're looking for:

template <typename T, typename V>
constexpr bool has_variant = false;

template <typename T, typename... Types>
constexpr bool has_variant<T, std::variant<Types...>> = (std::is_same_v<T, Types> || ...);

template <typename T, typename V>
constexpr bool has_variant<T, V&> = has_variant<T, V>;

and then

            std::visit([&](const auto& v)
            {
                if constexpr (has_variant<decltype(v), T>)
                {
                    result = std::get<T>(v);
                    found = true;
                }
                else
                    found = false;
            }, elem->second);

All that being said, this whole construction appears to me a pretty brittle solution for a problem that can probably be solved better in a different way. I would recommend to reconsider your approach here…

Upvotes: 2

Alan Birtles
Alan Birtles

Reputation: 36379

You can use a second level of visit to descend into your nested variant:

template < typename T >
struct Getter
{
    T& value;
    bool& found;
    void operator()(const T& t)
    {
        value = t;
        found = true;
    }
    template < typename U >
    void operator()(const U& u) {}
};

if(elem != m_internal_map.end()) {
   Getter< T > getter = { result, found };
   std::visit( getter, elem->second );
}

If the type isn't the desired type then Getter will ignore it. Note that Getter will also capture values convertible to T so might not be what you want.

Using a single level of variant would definitely be simpler.

Upvotes: 0

Related Questions