Как я могу использовать вариативные шаблоны С++ 11 для определения кортежей-векторов, поддерживаемых кортежами-векторами?
Предположим, что у меня есть связка векторов:
vector<int> v1;
vector<double> v2;
vector<int> v3;
все одинаковой длины. Теперь для каждого индекса я я хотел бы иметь возможность рассматривать (v1 [i], v2 [i], v3 [i]) как набор, и, возможно, передавать его. На самом деле, я хочу иметь вектор-кортежи, а не кортежи-векторы, используя которые я могу сделать выше. (В терминах C я могу сказать массив-структуры, а не структуру массивов). Я не хочу выполнять какое-либо переупорядочение данных (думаю: действительно длинные векторы), т.е. Новый вектор поддерживается отдельными векторами, которые я передаю. Пусть.
Теперь я хочу, чтобы класс, который я пишу (назовите его ToVBackedVoT
из-за отсутствия лучшего имени), чтобы поддерживать любой произвольный выбор векторов для его возврата (не только 3, не int, double и int, а не все просто скаляры). Я хочу, чтобы vector-of-tuples изменялся, и никаких копий для построения/присвоений не требуется.
Если я правильно понимаю, вариативные шаблоны и новый тип std::tuple
в С++ 11 - это средство для этого (предполагая, что мне не нужны нетипизированные массивы void*
и т.д.). Однако я только их не знаю и никогда с ними не работал. Можете ли вы помочь мне набросать, как будет выглядеть такой класс? Или как, учитывая
template <typename ... Ts>
Я могу выразить что-то вроде "список аргументов шаблона, являющийся заменой каждого имени в исходных аргументах шаблона вектором элементов этого типа"?
Примечание. Я думаю, что, возможно, мне захочется позже присоединиться к дополнительным векторам к векторам поддержки, сделав экземпляр ToVBackedVoT<int, double, int>
в, например, экземпляр ToVBackedVoT<int, double, int, unsigned int>
. Поэтому помните об этом, когда отвечаете. Это не критично важно.
Ответы
Ответ 1
Одна из идей - сохранить хранилище в стиле "struct of array" в виде векторов для хорошей производительности, если для определенной задачи используется только подмножество полей. Затем для каждого типа задач, требующих другого набора полей, вы можете написать небольшую оболочку вокруг некоторых из этих векторов, предоставляя вам хороший интерфейс итератора с произвольным доступом, аналогичный тому, что поддерживает std::vector
.
Что касается синтаксиса вариационных шаблонов, то как класс оболочки (без каких-либо итераторов) может выглядеть так:
template<class ...Ts> // Element types
class WrapMultiVector
{
// references to vectors in a TUPLE
std::tuple<std::vector<Ts>&...> m_vectors;
public:
// references to vectors in multiple arguments
WrapMultiVector(std::vector<Ts> & ...vectors)
: m_vectors(vectors...) // construct tuple from multiple args.
{}
};
Чтобы построить такой шаблонный класс, часто предпочитали иметь тип шаблона, который вычитал бы вспомогательную функцию (похожую на те функции make_{pair|tuple|...}
в std
):
template<class ...Ts> // Element types
WrapMultiVector<Ts...> makeWrapper(std::vector<Ts> & ...vectors) {
return WrapMultiVector<Ts...>(vectors...);
}
Вы уже видите разные типы "распаковки" списка типов.
Добавление итераторов, подходящих для вашего приложения (вы запросили, в частности, итераторы произвольного доступа), не так просто. Старт может быть перенаправлен только на итераторы, которые вы можете распространять на итераторы произвольного доступа.
Следующий класс итератора может быть сконструирован с использованием кортежа итераторов элементов, который увеличивается и разыменовывается для получения кортежа ссылок на элементы (важно для доступа на чтение и запись).
class iterator {
std::tuple<typename std::vector<Ts>::iterator...> m_elemIterators;
public:
iterator(std::tuple<typename std::vector<Ts>::iterator...> elemIterators)
: m_elemIterators(elemIterators)
{}
bool operator==(const iterator &o) const {
return std::get<0>(m_elemIterators) == std::get<0>(o.m_elemIterators);
}
bool operator!=(const iterator &o) const {
return std::get<0>(m_elemIterators) != std::get<0>(o.m_elemIterators);
}
iterator& operator ++() {
tupleIncrement(m_elemIterators);
return *this;
}
iterator operator ++(int) {
iterator old = *this;
tupleIncrement(m_elemIterators);
return old;
}
std::tuple<Ts&...> operator*() {
return getElements(IndexList());
}
private:
template<size_t ...Is>
std::tuple<Ts&...> getElements(index_list<Is...>) {
return std::tie(*std::get<Is>(m_elemIterators)...);
}
};
Для демонстрационных целей в этом коде два разных шаблона, которые "перебирают" по кортежу, чтобы применить некоторую операцию или построить новый кортеж с некоторой epxression для вызова для каждого элемента. Я использовал оба варианта, чтобы продемонстрировать альтернативы; вы также можете использовать только второй метод.
-
tupleIncrement
: вы можете использовать вспомогательную функцию, которая использует метапрограммирование для индексации одной записи и продвижения индекса на единицу, а затем вызов рекурсивной функции, пока индекс не окажется в конце кортежа (тогда существует специальная реализация, которая запускается с использованием SFINAE). Функция определена вне класса, а не выше; вот его код:
template<std::size_t I = 0, typename ...Ts>
inline typename std::enable_if<I == sizeof...(Ts), void>::type
tupleIncrement(std::tuple<Ts...> &tup)
{ }
template<std::size_t I = 0, typename ...Ts>
inline typename std::enable_if<I < sizeof...(Ts), void>::type
tupleIncrement(std::tuple<Ts...> &tup)
{
++std::get<I>(tup);
tupleIncrement<I + 1, Ts...>(tup);
}
Этот метод нельзя использовать для назначения кортежей ссылок в случае operator*
, потому что такой кортеж должен быть немедленно инициализирован ссылками, что невозможно с помощью этого метода. Поэтому нам нужно что-то еще для operator*
:
-
getElements
: Эта версия использует список индексов (fooobar.com/questions/353495/...), который также расширяется, а затем вы можете использовать std::get
с индексом чтобы расширить полные выражения. IndexList
при вызове функции создает соответствующий индексный список, который требуется только для вывода типа шаблона, чтобы получить те Is...
. Тип может быть определен в классе-оболочке:
// list of indices
typedef decltype(index_range<0, sizeof...(Ts)>()) IndexList;
Более полный код с небольшим примером можно найти здесь: http://ideone.com/O3CPTq
Открытые проблемы:
-
Если векторы имеют разные размеры, код не работает. Лучше было бы проверить все "концевые" итераторы на равенство; если один итератор "в конце", мы также "в конце"; но для этого потребуется некоторая логика больше, чем operator==
и operator!=
, если только не будет "подделать" ее; что operator!=
может возвращать false, как только любой оператор неравномерен.
-
Решение не является const-правильным, например. нет const_iterator
.
-
Добавление, вставка и т.д. невозможна. Класс wrapper может добавить функцию insert
или или/или push_back
, чтобы сделать ее схожей с std::vector
. Если ваша цель состоит в том, что она синтаксически совместима с вектором кортежей, переопределите все соответствующие функции из std::vector
.
-
Недостаточно тестов;)
Ответ 2
Альтернативой всем манипуляциям с вариационным шаблоном является использование boost::zip_iterator
для этой цели. Например (непроверенный):
std::vector<int> ia;
std::vector<double> d;
std::vector<int> ib;
std::for_each(
boost::make_zip_iterator(
boost::make_tuple(ia.begin(), d.begin(), ib.begin())
),
boost::make_zip_iterator(
boost::make_tuple(ia.end(), d.end(), ib.end())
),
handle_each()
);
Где ваш обработчик выглядит следующим образом:
struct handle_each :
public std::unary_function<const boost::tuple<const int&, const double&, const int&>&, void>
{
void operator()(const boost::tuple<const int&, const double&, const int&>& t) const
{
// Now you have a tuple of the three values across the vector...
}
};
Как вы можете видеть, довольно просто расширить это, чтобы поддерживать произвольный набор векторов.
Ответ 3
Вы можете использовать что-то вроде:
#if 1 // Not available in C++11, so write our own
// class used to be able to use std::get<Is>(tuple)...
template<int... Is>
struct index_sequence { };
// generator of index_sequence<Is>
template<int N, int... Is>
struct make_index_sequence : make_index_sequence<N - 1, N - 1, Is...> { };
template<int... Is>
struct make_index_sequence<0, Is...> : index_sequence<Is...> { };
#endif
// The 'converting' class
// Note that it doesn't check that vector size are equal...
template<typename ...Ts>
class ToVBackedVoT
{
public:
explicit ToVBackedVoT(std::vector<Ts>&... vectors) : data(vectors...) {}
std::tuple<const Ts&...> operator [] (unsigned int index) const
{
return at(index, make_index_sequence<sizeof...(Ts)>());
}
std::tuple<Ts&...> operator [] (unsigned int index)
{
return at(index, make_index_sequence<sizeof...(Ts)>());
}
private:
template <int... Is>
std::tuple<const Ts&...> at(unsigned int index, index_sequence<Is...>) const
{
return std::tie(std::get<Is>(data)[index]...);
}
template <int... Is>
std::tuple<Ts&...> at(unsigned int index, index_sequence<Is...>)
{
return std::tie(std::get<Is>(data)[index]...);
}
private:
std::tuple<std::vector<Ts>&...> data;
};
И для итерации создайте "IndexIterator", как в fooobar.com/questions/353496/...
Чтобы соединить дополнительные векторы, вы должны создать другой ToVBackedVoT
как std::tuple_cat
для std::tuple
Ответ 4
От выяснения вопроса о том, как это будет использоваться (код, который берет кортеж), я собираюсь предложить это вместо.
//give the i'th element of each vector
template<typename... Ts>
inline tuple<Ts&...> ith(size_t i, vector<Ts>&... vs){
return std::tie(vs[i]...);
}
Есть предложение, чтобы пакеты параметров сохранялись как члены классов (N3728). Используя это, здесь некоторый непроверенный и непроверенный код.
template<typename... Types>
class View{
private:
vector<Types>&... inner;
public:
typedef tuple<Types&...> reference;
View(vector<Types>&... t): inner(t...) {}
//return smallest size
size_t size() const{
//not sure if ... works with initializer lists
return min({inner.size()...});
}
reference operator[](size_t i){
return std::tie(inner[i]...);
}
};
Итерация:
public:
iterator begin(){
return iterator(inner.begin()...);
}
iterator end(){
return iterator(inner.end()...);
}
//for .begin() and .end(), so that ranged-based for can be used
class iterator{
vector<Types>::iterator... ps;
iterator(vector<Types>::iterator... its):ps(its){}
friend View;
public:
//pre:
iterator operator++(){
//not sure if this is allowed.
++ps...;
//use this if not:
// template<typename...Types> void dummy(Types... args){} //global
// dummy(++ps...);
return *this;
}
iterator& operator--();
//post:
iterator operator++(int);
iterator operator--(int);
//dereference:
reference operator*()const{
return std::tie(*ps...);
}
//random access:
iterator operator+(size_t i) const;
iterator operator-(size_t i) const;
//need to be able to check end
bool operator==(iterator other) const{
return std::make_tuple(ps...) == std::make_tuple(other.ps...);
}
bool operator!=(iterator other) const{
return std::make_tuple(ps...) != std::make_tuple(other.ps...);
}
};
Ответ 5
Преобразование в std:: набор векторов (вектор:: итераторы):
#include <iostream>
#include <vector>
// identity
// ========
struct identity
{
template <typename T>
struct apply {
typedef T type;
};
};
// concat_operation
// ================
template <typename Operator, typename ...> struct concat_operation;
template <
typename Operator,
typename ...Types,
typename T>
struct concat_operation<Operator, std::tuple<Types...>, T>
{
private:
typedef typename Operator::template apply<T>::type concat_type;
public:
typedef std::tuple<Types..., concat_type> type;
};
template <
typename Operator,
typename ...Types,
typename T,
typename ...U>
struct concat_operation<Operator, std::tuple<Types...>, T, U...>
{
private:
typedef typename Operator::template apply<T>::type concat_type;
public:
typedef typename concat_operation<
Operator,
std::tuple<Types..., concat_type>,
U...>
::type type;
};
template <
typename Operator,
typename T,
typename ...U>
struct concat_operation<Operator, T, U...>
{
private:
typedef typename Operator::template apply<T>::type concat_type;
public:
typedef typename concat_operation<
Operator,
std::tuple<concat_type>,
U...>
::type type;
};
// ToVectors (ToVBackedVoT)
// =========
template <typename ...T>
struct ToVectors
{
private:
struct to_vector {
template <typename V>
struct apply {
typedef typename std::vector<V> type;
};
};
public:
typedef typename concat_operation<to_vector, T...>::type type;
};
// ToIterators
// ===========
template <typename ...T>
struct ToIterators;
template <typename ...T>
struct ToIterators<std::tuple<T...>>
{
private:
struct to_iterator {
template <typename V>
struct apply {
typedef typename V::iterator type;
};
};
public:
typedef typename concat_operation<to_iterator, T...>::type type;
};
int main() {
typedef ToVectors<int, double, float>::type Vectors;
typedef ToVectors<Vectors, int, char, bool>::type MoreVectors;
typedef ToIterators<Vectors>::type Iterators;
// LOG_TYPE(Vectors);
// std::tuple<
// std::vector<int, std::allocator<int> >,
// std::vector<double, std::allocator<double> >,
// std::vector<float, std::allocator<float> > >
// LOG_TYPE(Iterators);
// std::tuple<
// __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >,
// __gnu_cxx::__normal_iterator<double*, std::vector<double, std::allocator<double> > >,
// __gnu_cxx::__normal_iterator<float*, std::vector<float, std::allocator<float> > > >
}
Ответ 6
В качестве альтернативы, аналогичной boost::zip_iterator
, я написал функцию zip
с очень простым интерфейсом:
vector<int> v1;
vector<double> v2;
vector<int> v3;
auto vec_of_tuples = zip(v1, v2, v3);
Например, перебираем эти кортежи:
for (auto tuple : zip(v1, v2, v3)) {
int x1; double x2; int x3;
std::tie(x1, x2, x3) = tuple;
//...
}
Здесь zip()
принимает любое количество диапазонов любого типа. Он возвращает адаптер, который можно рассматривать как лениво оцениваемый диапазон по кортежу элементов, происходящих из обернутых диапазонов.
Адаптер является частью моей функциональной библиотеки стиля Haskell "fn" и реализован с использованием вариативных шаблонов.
В настоящее время он не поддерживает модификацию значений исходного диапазона через адаптер из-за дизайна библиотеки (она предназначена для использования с не изменяемыми диапазонами, как в функциональном программировании).
A краткое описание о том, как это делается: zip(...)
возвращает объект-адаптер, который реализует begin()
и end()
, возвращая объект-итератор. Итератор содержит кортеж итераторов для обернутых диапазонов. Приращение итератора увеличивает все завернутые итераторы (которые реализуются с помощью списка индексов и распаковывают увеличивающееся выражение в ряд выражений: ++std::get<I>(iterators)...
). Разделение итератора будет уменьшать все завернутые итераторы и передать их на std::make_tuple
(который также реализуется как распаковка выражения *std::get<I>(iterators)...
).
P.S. Его реализация основана на множестве идей, исходящих из ответов на этот вопрос.