4
#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.

JeJo
  • 30,635
  • 6
  • 49
  • 88
Tim
  • 403
  • 4
  • 9

2 Answers2

4

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
JeJo
  • 30,635
  • 6
  • 49
  • 88
  • Indeed, `std::transform` looks like it is exactly what I was asking for, at least for `std::set`. However, how well does it work with `Boost::flat_set`? I’d be worried about the performance. – Tim Sep 30 '19 at 13:28
  • @Tim I am not familiar with Boost::fat_set, but in the case of performance doubts, just benchmark the case with any other possible solutions what you know. Other than that, unfortunately i could not say any other options. – JeJo Sep 30 '19 at 14:42
  • @JeJo `flat_set` is the api of `set` backed by a `vector` instead of a tree – Caleth Oct 02 '19 at 09:58
  • @Caleth Thanks for the info. Unfortunately, I didn't get a chance for `boost` ing my code. – JeJo Oct 02 '19 at 12:35
2

Similar to std::transform, you can also use boost::transform (or it's pipe variant boost::adaptors::transformed), which takes the whole container, rather than it's begin and end iterator. It returns a view, which you can initialise myset with.

auto view = boost::transform(myvector, std::mem_fn(&myclass::get_a));
auto myset = std::set<int>(view.begin(), view.end());

You can also use boost::copy_range to range-construct a container from an rvalue range.

auto to_a = std::mem_fn(&myclass::get_a);
auto myset = boost::copy_range<std::set<int>>(myvector | boost::adaptors::transformed(to_a));
Caleth
  • 52,200
  • 2
  • 44
  • 75