简体   繁体   中英

How to filter out elements of certain data types in a vector of std::variant?

I have a std::vector of std::variant elements with type int or std::set<int> . I want to loop over this vector and insert an extra item if the iterated element is of type std::set<int> . However, it seems that querying the index at run-time is not allowed. How can I achieve this?

#include <variant>
#include <set>
#include <vector>

int main()
{
    using Variants = std::variant<int, std::set<int>>;

    std::vector<Variants> var_vec;
    var_vec.push_back(999);
    std::set<int> a = {0,1,2};
    var_vec.push_back(a);

    for (int i = 0; i < var_vec.size(); ++i)
    {
        // if the element var_vec[i] is of type std::set<int>
        if (var_vec[i].index() == 1) var_vec[i].insert(888);   // !ERROR! How to achieve this?
    }
    
    return 0;
}

Error message:

error: '__gnu_cxx::__alloc_traits<std::allocator<std::variant<int, std::set<int, std::less<int>, std::allocator<int> > > >, std::variant<int, std::set<int, std::less<int>, std::allocator<int> > > >::value_type' {aka 'class std::variant<int, std::set<int, std::less<int>, std::allocator<int> > >'} has no member named 'insert'

There's nothing wrong with calling index() at runtime, this happens all the time. The real problem is:

var_vec[i].insert(888);

var_vec[i] is a std::variant . It does not have a method called insert() .

std::get<1>(var_vec[i]).insert(888);

This gives you the variant's set, which will happily allow you to insert() something.

One problem with this overall approach is that if you, for some reason, want to modify your variant, and that std::set is no longer its 2nd alternative, for some reason, all of the above logic will break again.

You should consider using std::holds_alternative , instead of index() , and using a type with std::get instead of an index, which will automatically adapt to this kind of a change.

I want to loop over this vector and insert an extra item if the iterated element is of type std::set. However, it seems that querying the index at run-time is not allowed. How can I achieve this?

You don't need to check the type at runtime, you could just use std::visit() and check if the variant's type is std::set<int> at compile-time:

// Others headers go here...
#include <type_traits>

// ...

for (int i = 0; i < var_vec.size(); ++i)
    // if the element var_vec[i] is of type std::set<int>
    std::visit([](auto&& arg) {
        if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, std::set<int>>)
            arg.insert(888);
    }, var_vec[i]);

The filtering can be done via ranges::views::filter , and we can use std::visit to make up a predicate.

To do so we need to feed std::visit with a predicate on the types. A predicate on types can be made up as a set of overlaoded functions that each takes a type and all return a bool ; you can create it via boost::hana::overload .

However, to filter the variants we need to feed std::visit only with its first argument, and to leave its second parameter free; one way to do it is to wrap std::visit in a nested lambda, like this:

    auto visit_with = [](auto const& fs){
      return [&fs](auto const& xs) {
        return std::visit(fs, xs);
      };
    };

Another way is to use BOOST_HOF_LIFT and boost::hana::curry the result to make it partially applicabale, which is all done in one line:

    auto visit_with = curry<2>(BOOST_HOF_LIFT(std::visit));

Here's the complete code:

#include <boost/hana/functional/overload.hpp>
#include <boost/hana/functional/partial.hpp>
#include <iostream>
#include <range/v3/view/filter.hpp>
#include <set>
#include <variant>
#include <vector>
using namespace boost::hana;
using namespace ranges::views;
int main()
{
    using Variants = std::variant<int, std::set<int>>;

    std::vector<Variants> var_vec;
    var_vec.push_back(999);
    std::set<int> a = {0,1,2};
    var_vec.push_back(a);

    // predicate
    auto constexpr true_for_sets = overload(
          [](std::set<int> const&){ return true; },
          [](auto const&){ return false; });

    // function object wrapping std::visit
    auto visit_with = curry<2>(BOOST_HOF_LIFT(std::visit));

    // filter
    auto var_vec_out = var_vec | filter(visit_with(true_for_sets));
}

And this is all about the filtering you mention in the title. However, in the body of the question, you're not actually doing a filtering, but something else, as you are modifying the set as you traverse it.

As was already pointed out you're calling insert on an std::variant and not the std::set .

One approach I'd use is using std::get_if like so:

#include <variant>
#include <set>
#include <vector>

int main( ) {
    using Variants = std::variant<int, std::set<int>>;

    std::vector<Variants> var_vec;    
    var_vec.push_back(999);
    std::set<int> a = {0,1,2};
    var_vec.push_back(a);

    for ( auto& v : var_vec ) {    
        // If the variant does not hold a set, get_if returns a nullptr.    
        if ( auto set{ std::get_if<std::set<int>>( &v ) } ) {
            set->insert( 888 );
        }
    }
    // Or you could use holds_alternative with get, but this isn't as clean.
    for ( auto& v : var_vec ) {        
        if ( std::holds_alternative<std::set<int>>( v ) ) {
            auto set{ std::get<std::set<int>>( v ) };
            set.insert( 999 );
        }
    }
}

This allows you to both test for the type and use the contained alternative.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM