Search code examples
c++algorithmc++11stdvectorstdset

How to construct a std::set or Boost flat_set from objects’ data members?


#include <iostream>
#include <vector>
#include <set>

class Myclass
{
    int member_a;
    int member_b;
public:
    Myclass() {};
    Myclass(int a_init, int b_init) : member_a(a_init), member_b(b_init) {};

    operator int() const {      return member_a;    }
    int get_a() const {     return member_a;    }
};

int main()
{
    auto myvector = std::vector<Myclass>({ {1, 0}, {2, 0}, {2, 0}, {3, 0} });
    auto myset = std::set<int>(myvector.begin(), myvector.end());
    for (auto element : myset) {
        std::cout << "element: " << element << "\n";
    }
}

As you can see, I am constructing a std::set that contains only a particular data member of each object in a std::vector. I achieve this by using operator int().

However, I dislike this solution because it is not very readable and creates potential pitfalls, and I may also want to create a set of only the member_b s.

Is there a way of constructing the set using get_a() instead of the operator int(), without using a loop? I’d also like to avoid creating a temporary vector that contains only the member_a's.

The same issue is particularly relevant for constructing a Boost::flat_set which, as far as I understand, would re-sort unnecessarily if the elements are added one-by-one in a loop.


Solution

  • You can use std::transform to insert the desired members to myset instead of using operator int(). (See live online)

    #include <algorithm> // std::transform
    #include <iterator>  // std::inserter
    
    std::transform(myvector.cbegin(), myvector.cend()
        , std::inserter(myset, myset.begin())
        , [](const auto& cls) { return cls.get_a(); }
    );
    

    Generic enough?. Okay, in order to make it more generic, you can put it into a function, in which pass the vector of Myclass, myset to be filled, and the member function pointer which needed to be called. (See live online)

    #include <algorithm>  // std::transform
    #include <iterator>   // std::inserter
    #include <functional> // std::invoke
    #include <utility>    // std::forward
    
    using MemFunPtrType = int(Myclass::*)() const; // convenience type
    
    void fillSet(const std::vector<Myclass>& myvector, std::set<int>& myset, MemFunPtrType func)
    {
        std::transform(myvector.cbegin(), myvector.cend()
            , std::inserter(myset, myset.begin())
            , [func](const Myclass& cls) { 
                   return (cls.*func)(); 
                   // or in C++17 simply invoke the func with each instace of the MyClass
                   // return std::invoke(func, cls);
            }
        );
    }
    

    Or completely generic using templates, one could: (See live online)

    template<typename Class, typename RetType, typename... Args>
    void fillSet(const std::vector<Class>& myvector
        , std::set<RetType>& myset
        , RetType(Class::*func)(Args&&...)const
        , Args&&... args)
    {
        std::transform(myvector.cbegin(), myvector.cend()
            , std::inserter(myset, myset.begin())
            , [&](const Myclass& cls) { return std::invoke(func, cls, std::forward<Args>(args)...);  }
        );
    }
    

    Now you fill the myset like.

    fillSet(myvector, myset, &Myclass::get_a); // to fill with member a
    fillSet(myvector, myset, &Myclass::get_b); // to fill with member b
    

    Here is the full working example:

    #include <iostream>
    #include <vector>
    #include <set>
    #include <algorithm>  // std::transform
    #include <iterator>   // std::inserter
    #include <functional> // std::invoke
    #include <utility>    // std::forward
    
    class Myclass
    {
        int member_a;
        int member_b;
    public:
        Myclass(int a_init, int b_init) : member_a{ a_init }, member_b{ b_init } {};
        int get_a() const noexcept { return member_a;   }
        int get_b() const noexcept { return member_b;   }
    };
    
    template<typename Class, typename RetType, typename... Args>
    void fillSet(const std::vector<Class>& myvector
        , std::set<RetType>& myset
        , RetType(Class::*func)(Args&&...)const
        , Args&&... args)
    {
        std::transform(myvector.cbegin(), myvector.cend()
            , std::inserter(myset, myset.begin())
            , [&](const Myclass& cls) { return std::invoke(func, cls, std::forward<Args>(args)...);  }
        );
    }
    
    int main()
    {
        auto myvector = std::vector<Myclass>({ {1, 0}, {2, 0}, {2, 0}, {3, 0} });
        std::set<int> myset;
    
        std::cout << "Filling with member a\n";
        fillSet(myvector, myset, &Myclass::get_a);
        for (auto element : myset)  std::cout << "element: " << element << "\n";
    
        std::cout << "Filling with member b\n"; 
        myset.clear();
        fillSet(myvector, myset, &Myclass::get_b);
        for (auto element : myset) std::cout << "element: " << element << "\n";
    
    }
    

    Output:

    Filling with member a
    element: 1
    element: 2
    element: 3
    Filling with member b
    element: 0