Patryk
Patryk

Reputation: 24092

Transform vector of Class to vector of decltype(Class::name()) by calling name() on every element

Having the following struct MyStruct:

struct MyStruct {                                                                                                                                                                                      
  explicit MyStruct(std::string name) : name_(name){}
  std::string name() const { return name_; }
private:
  std::string name_;
};

I would like to transform a std::vector of MyStructs to a std::vector<std::string>. I have managed to do it like so:

#include <iostream>
#include <string>
#include <vector>

#include <boost/range/adaptor/transformed.hpp>

struct MyStruct {
  explicit MyStruct(std::string name) : name_(name){}
  std::string name() const { return name_; }
private:
  std::string name_;
};

int main() {
  std::vector<MyStruct> vm;
  vm.emplace_back("asd");
  vm.emplace_back("qwe");
  vm.emplace_back("zxc");

  using namespace boost::adaptors;
  auto vs = vm | transformed([](const MyStruct& c){return c.name();});

  for (const auto& c : vs) std::cout << c << std::endl;
}

but then vs is not a vector but a:

const boost::range_detail::transformed_range<(lambda at /tmp/main.cpp:21:36), std::vector<MyStruct, std::allocator<MyStruct> > > => const boost::range_detail::transformed_range<(lambda at /tmp/main.cpp:21:36), std::vector<MyStruct, std::allocator<MyStruct> > >

How can I achieve this? Preferably I would like to do it while initializing the vector - not by declaring vector and then std::copy or something similar.

I know that I can do something similar to:

std::vector<std::string> vv; 
vv.reserve(boost::size(vs));
boost::copy(vs, std::back_inserter(vv));

but I'd like a one step initialization (preferably with a const qualifier).

I believe this is some sort of a high order (functional) map function that I need or an std/boost/handwritten equivalent.

Upvotes: 3

Views: 357

Answers (4)

MikeMB
MikeMB

Reputation: 21156

Not as nice as solution based on ranges-v3, but it has the advantage to not need any 3rd party library:

const auto vs = [&]{
                    std::vector<std::string> t(mv.size());
                    std::transform(mv.begin(), mv.end(), t.begin(), [](auto& s) {return s.name(); });
                    return t;
                }();

You can of course use reserve and back_inserter to prevent default construction, but I'd actually time the code before prefering one over the other.

Upvotes: 0

R Sahu
R Sahu

Reputation: 206577

You can also create a helper class that acts as wrapper around std::vector::iterator that can be used to construct a std::vector<std::string> from a std::vector<MyStruct>.

#include <iostream>
#include <string>
#include <vector>

struct MyStruct {
  explicit MyStruct(std::string name) : name_(name){}
  std::string name() const { return name_; }
private:
  std::string name_;
};

// A minimal wrapper around std::vector::iterator
// to help with constructing a std::vector<std::string> from a 
// std::vector<MyStruct>

struct Iter 
{
   using iterator_category = std::input_iterator_tag;
   using value_type = std::string;
   using pointer = std::string*;
   using reference = std::string;
   using difference_type = long;

   Iter(std::vector<MyStruct>::iterator iter) : iter_(iter) {}
   std::string operator*() const { return (*iter_).name(); }
   bool operator!=(Iter const& rhs) const { return this->iter_ != rhs.iter_; }
   Iter& operator++() { ++iter_; return *this;}

   std::vector<MyStruct>::iterator iter_;
};

int main() {
  std::vector<MyStruct> vm;
  vm.emplace_back("asd");
  vm.emplace_back("qwe");
  vm.emplace_back("zxc");

  std::vector<std::string> vs(Iter(vm.begin()), Iter(vm.end()));
  for (const auto& c : vs) std::cout << c << std::endl;
}

Upvotes: 1

Barry
Barry

Reputation: 303017

I'd recommend using range-v3, which is basically the new and improved Boost.Ranges. There, it's just:

std::vector<std::string> vs = vm | ranges::view::transform(&MyStruct::name);

Note that if you captured this by auto, you wouldn't get a vector. The resulting object has a conversion operator.

With Boost.Ranges, you can't do that natively, but you can write your own piped adaptor:

struct make_vector_t
{
    template <class R>
    friend auto operator|(R&& range, make_vector_t)
    {
        using std::begin;
        using std::end;

        using value_type = typename iterator_traits<decltype(begin(range))>::value_type;
        return std::vector<value_type>(begin(range), end(range));
    }
} constexpr make_vector{};

and just use it:

auto vs = vm 
    | transformed([](const MyStruct& c){return c.name();});
    | make_vector;

Upvotes: 5

Tareq A. Siraj
Tareq A. Siraj

Reputation: 424

If range-v3 as Barry mentioend is not an option, why not just wrap your multi-line algorithm into a function call? E.g.

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

struct MyStruct {
  MyStruct(std::string n) : m_name(n) {}
  std::string name() const { return m_name; }

  std::string m_name;
};

template <typename T> auto ExtractNames(const std::vector<T> &vm) {
  std::vector<decltype(std::declval<T>().name())> result(vm.size());
  std::transform(vm.begin(), vm.end(), result.begin(),
                 [](const T &v) { return v.name(); });

  return result;
}

int main() {
  std::vector<MyStruct> vm{{"asd"}, {"qwe"}, {"zxc"}};
  auto vs = ExtractNames(vm);

  for (auto &elem : vs) {
    std::cout << elem << "\n";
  }

  return 0;
}

Upvotes: 0

Related Questions