Единая инициализация кортежем

Сегодня я столкнулся с ситуацией, когда у меня есть вектор кортежей, где кортежи могут содержать несколько записей. Теперь я хотел преобразовать свой вектор кортежей в вектор объектов, чтобы записи кортежей точно соответствовали юниформ-инициализации моего объекта.

Следующий код делает эту работу за меня, но он немного неуклюж. Я спрашиваю себя, возможно ли получить общее решение, которое может создавать объекты, если кортежи точно соответствуют единообразному порядку инициализации объектов.

Это может быть очень желательной функциональностью, когда количество передаваемых параметров растет.

#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;
}

person Aleph0    schedule 05.07.2019    source источник


Ответы (3)


Вот ненавязчивая версия (т. е. не касающаяся Object), которая извлекает количество указанных элементов данных. Обратите внимание, что это зависит от агрегатной инициализации.

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>{});
}

Вы вызываете его так:

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

Или без лямбда-обертки:

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

Как указал @Deduplicator, приведенные выше вспомогательные шаблоны реализуют части std::apply, которые можно использовать вместо этого.

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); });
person lubgr    schedule 05.07.2019
comment
Да, но мне нужно найти лучший способ передать третий параметр шаблона - person lubgr; 05.07.2019
comment
@lubgr: Это еще ближе к тому, чего я хочу. В частности, это функция многократного использования, которая не предполагает особого типа объекта. Отличное решение. Можно ли заменить std::transform на std::copy? - person Aleph0; 05.07.2019
comment
Нет, std::copy не работает, потому что происходит преобразование, вы конструируете один тип из другого, что не может быть неявным, когда выполняется с помощью функции (шаблона), внешней по отношению к целевому типу. - person lubgr; 05.07.2019
comment
Хорошо. Я просто потому, что в решении @Stack Danny можно было заменить std::transform на std::copy. Но ваше решение — это совсем другая история. - person Aleph0; 05.07.2019
comment
Почему бы не использовать std::apply()? - person Deduplicator; 05.07.2019

Предоставьте Object конструктор std::tuple. Вы можете использовать std::tie для назначения участников:

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

Теперь он создается автоматически:

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

Чтобы уменьшить объем копирования, вы можете заменить auto v на const auto& v и заставить конструктор принимать const std::tuple<Args...>& t.


Кроме того, рекомендуется обращаться к исходному контейнеру через const итератор:

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

person Stack Danny    schedule 05.07.2019
comment
Очень круто! Кстати. что даже делает std::transform std::copy. - person Aleph0; 05.07.2019
comment
Не могли бы вы объяснить, как работает этот конструктор? А шаблон для чего? - person BartekPL; 05.07.2019
comment
@BartekPL это типичный макет для std::tuple. Этот шаблон называется пакетом параметров. std::tie создает ссылочный кортеж, который делает возможным присваивание. - person Stack Danny; 05.07.2019

Начиная с C++17, вы можете использовать std::make_from_tuple:

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

Примечание: требуется соответствующий конструктор для Object.

person Jarod42    schedule 05.07.2019