26

Today, I arrived at a situation, where I have a vector of tuples, where the tuples might contain several entries. Now I wanted to convert my vector of tuples to a vector of objects, such that the entries of the tuples will exactly match the uniform initialization of my object.

The following code does the job for me, but it is a bit clumsy. I'm asking myself if it might be possible to derive a generic solution that can construct the Objects if the tuples matches exactly the uniform initialization order of the objects.

This might be a very desirable functionality, when the number of parameters to pass grows.

#include <vector>
#include <tuple>
#include <string>
#include <algorithm>

struct Object
{
    std::string s;
    int i;
    double d;
};

int main() {
    std::vector<std::tuple<std::string, int, double>> values = { {"A",0,0.},{"B",1,1.} };

    std::vector<Object> objs;
    std::transform(values.begin(), values.end(), std::back_inserter(objs), [](auto v)->Object
        {
        // This might get tedious to type, if the tuple grows
            return { std::get<0>(v), std::get<1>(v), std::get<2>(v) };
           // This is my desired behavior, but I don't know what magic_wrapper might be
            // return magic_wrapper(v);
        });

    return EXIT_SUCCESS;
}
Aleph0
  • 5,816
  • 4
  • 29
  • 80

3 Answers3

18

Provide Object an std::tuple constructor. You can use std::tie to assign your members:

template<typename ...Args>
Object(std::tuple<Args...> t) {
    std::tie(s, i, d) = t;
}

Now it gets automatically constructed:

std::transform(values.begin(), values.end(), std::back_inserter(objs), 
    [](auto v) -> Object {
        return { v };
    });

To reduce the amount of copying you might want to replace auto v with const auto& v and make the constructor accept a const std::tuple<Args...>& t.


Also, it's good practise to access the source container via const iterator:

std::transform(values.cbegin(), values.cend(), std::back_inserter(objs), ...

Stack Danny
  • 7,754
  • 2
  • 26
  • 55
  • Very cool! Btw. that even makes the `std::transform` a `std::copy`. – Aleph0 Jul 05 '19 at 08:16
  • Could you explain how this constructor works? And what the template is for ? – BartekPL Jul 05 '19 at 08:19
  • 4
    @BartekPL this is the typical layout for an `std::tuple`. This template is called a [parameter pack](https://en.cppreference.com/w/cpp/language/parameter_pack). `std::tie` constructs a reference tuple which makes an assignement possible. – Stack Danny Jul 05 '19 at 08:22
13

Here is a non-intrusive version (i.e. not touching Object) that extracts the number of specified data members. Note that this relies on aggregate initialization.

template <class T, class Src, std::size_t... Is>
constexpr auto createAggregateImpl(const Src& src, std::index_sequence<Is...>) {
   return T{std::get<Is>(src)...};
}

template <class T, std::size_t n, class Src>
constexpr auto createAggregate(const Src& src) {
   return createAggregateImpl<T>(src, std::make_index_sequence<n>{});
}

You invoke it like this:

std::transform(values.cbegin(), values.cend(), std::back_inserter(objs),
     [](const auto& v)->Object { return createAggregate<Object, 3>(v); });

Or, without the wrapping lambda:

std::transform(values.cbegin(), values.cend(), std::back_inserter(objs),
   createAggregate<Object, 3, decltype(values)::value_type>);

As @Deduplicator pointed out, the above helper templates implement parts of std::apply, which can be used instead.

template <class T>
auto aggregateInit()
{
   return [](auto&&... args) { return Object{std::forward<decltype(args)>(args)...}; };
}

std::transform(values.cbegin(), values.cend(), std::back_inserter(objs),
    [](const auto& v)->Object { return std::apply(aggregateInit<Object>(), v); });
lubgr
  • 37,368
  • 3
  • 66
  • 117
  • Yes, but I need to figure out the best way to pass the 3rd template parameter – lubgr Jul 05 '19 at 08:23
  • @lubgr: That's even more closer to what I desire. Especially, it is a reusable function, that doesn't assume a special kind of Object. Great Solution. Would it be possible to replace the `std::transform` with a `std::copy`? – Aleph0 Jul 05 '19 at 08:23
  • 3
    No, `std::copy` doesn't work, because there _is_ a transformation happening, you construct one type from another one, which can't be implicit when done with a function (template) exterior to the target type. – lubgr Jul 05 '19 at 08:26
  • Alright. I just because in the solution of @Stack Danny it was possible to replace the `std::transform` with an `std::copy`. But you solution is a complete different story to tell. – Aleph0 Jul 05 '19 at 08:27
  • 1
    Why not use `std::apply()`? – Deduplicator Jul 05 '19 at 12:33
12

Since C++17, you might use std::make_from_tuple:

std::transform(values.begin(),
               values.end(),
               std::back_inserter(objs),
               [](const auto& t)
        {
            return std::make_from_tuple<Object>(t);
        });

Note: requires appropriated constructor for Object.

Jarod42
  • 203,559
  • 14
  • 181
  • 302