如何使用 C++11 可变参数模板来定义由向量元组支持的元组向量?
Posted
技术标签:
【中文标题】如何使用 C++11 可变参数模板来定义由向量元组支持的元组向量?【英文标题】:How can I use C++11 variadic templates to define a vector-of-tuples backed by a tuple-of-vectors? 【发布时间】:2014-01-09 04:25:33 【问题描述】:假设我有一堆向量:
vector<int> v1;
vector<double> v2;
vector<int> v3;
长度相同。现在,对于每个索引 i,我希望能够将 (v1[i], v2[i], v3[i]) 视为一个元组,并可能传递它。事实上,我想要一个元组向量而不是向量元组,我可以使用它来完成上述操作。 (在 C 语言中,我可能会说结构数组而不是数组结构)。我不想影响任何数据重新排序(想想:真的很长的向量),即新向量由我传入的单个向量支持。让我们。
现在,我希望我编写的类(称其为 ToVBackedVoT
,因为没有更好的名称)支持任意选择向量来支持它(不仅仅是 3,不是 int、double 和 int,不是每个都只是标量)。我希望元组向量是可变的,并且不希望在构造/分配时复制。
如果我理解正确,可变参数模板和 C++11 中的新 std::tuple
类型是执行此操作的方法(假设我不想要无类型的 void*
数组等)。然而,我几乎不认识他们,也从未与他们合作过。你能帮我勾勒出这样一个班级的样子吗?或者如何,给定
template <typename ... Ts>
我可以表达类似“模板参数列表是用这种类型的元素向量替换原始模板参数中的每个类型名”之类的东西?
注意:我想我以后可能还希望能够将其他向量连接到支持向量,使 ToVBackedVoT<int, double, int>
的实例成为 ToVBackedVoT<int, double, int, unsigned int>
的实例。因此,在回答时请记住这一点。不过,这并不重要。
【问题讨论】:
additional column to the table
表示元组中的新类型?
您希望底层存储像结构体数组还是数组结构体?这至少在效率方面很重要,并且取决于您的用例。
@JohnZwinck 我猜他设计的全部目的是拥有一个数组结构以提高效率,但他希望在 style of 数组中有一个很好的访问结构。
您可以围绕任意数量的向量编写迭代器包装器。这个包装器仅用于遍历向量集,仅此而已。这样,您的表中可以有很多“列”,但有几个不同的迭代器包装器,每个使用不同的列集(类似于同一张表上 SQL 中的简单“视图”)。这对您来说是一个不错的选择吗?
@JohnZwinck:就像 leems 建议的那样。澄清了我关于这一点的问题。
【参考方案1】:
一个想法是将 storage 保持在“数组结构”样式中,以向量的形式在只有字段的一个子集用于特定任务时获得良好的性能。然后,对于需要一组不同字段的每种任务,您可以围绕这些向量中的 一些 编写一个轻量级包装器,为您提供一个类似于 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.
;
要构造这样一个模板类,通常最好有一个可用的模板类型扣除辅助函数(类似于std
中的那些make_pair|tuple|...
函数):
template<class ...Ts> // Element types
WrapMultiVector<Ts...> makeWrapper(std::vector<Ts> & ...vectors)
return WrapMultiVector<Ts...>(vectors...);
您已经看到了不同类型的“解包”类型列表。
添加适合您的应用程序的迭代器(您特别要求随机访问迭代器)并不容易。开始可以是只转发迭代器,您可以将其扩展到随机访问迭代器。
以下迭代器类能够使用元素迭代器的元组构造,递增和取消引用以获得元素元组references(对于读写访问很重要)。
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)...);
;
出于演示目的,此代码中有两种不同的模式,它们“迭代”一个元组,以便应用某些操作或构造一个新的元组,其中包含要调用的某些表达式每个元素。我同时使用了这两种方法来展示替代方案;你也可以只使用第二种方法。
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
:这个版本使用了一个索引列表(https://***.com/a/15036110/592323),它也被扩展了,然后你可以使用std::get
和索引列表来扩展完整的表达式。调用函数时的IndexList
实例化了一个适当的索引列表,该索引列表仅在模板类型推导时需要,以获取那些Is...
。类型可以在包装类中定义:
// list of indices
typedef decltype(index_range<0, sizeof...(Ts)>()) IndexList;
更完整的代码和一个小例子可以在这里找到:http://ideone.com/O3CPTq
未解决的问题是:
如果向量的大小不同,则代码会失败。最好检查所有“结束”迭代器是否相等;如果一个迭代器“结束”,我们也“结束”;但这需要比operator==
和operator!=
更多的逻辑,除非可以“伪造”它;这意味着只要 any 运算符不相等,operator!=
就会返回 false。
解决方案不是 const 正确的,例如没有const_iterator
。
无法进行追加、插入等操作。包装类可以添加一些insert
或和/或push_back
函数以使其与std::vector
类似地工作。如果您的目标是在语法上与元组向量兼容,请重新实现 std::vector
中的所有相关函数。
没有足够的测试;)
【讨论】:
std::tuple<std::vector<Ts...>&> m_vectors;
这个字符串对吗?
好收获;我在 ideone 中修复了这个问题,但没有“向后移植”它;)现在修复了,谢谢。
我不知道typename std::vector<Ts>::iterator...
。马上开始滥用它!【参考方案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...
;
如您所见,扩展它以支持任意向量集非常简单。
【讨论】:
我需要更频繁地使用 boost。简直太棒了。 那么我们可以简单地做for (const auto& tup : make_zip_iterator( make_tuple(ia.begin(), d.begin(), ib.begin()))
吗?
@JohnZwinck,我不这么认为,因为需要提供完整的范围(即每个向量的末端也必须作为元组提供),这就是为什么你必须依赖关于for_each
等算法
@Nim:啊,当然。我在考虑(但不是写!)关于 make_zip_iterator 的一个版本,它可以直接获取序列并在内部调用 begin()/end(),然后公开它自己的 begin()/end()。我想这对大多数 C++ 人来说有点太愚蠢了。
我喜欢 Boost。每当我看到“当然,137th-B 后管只会发射你想要的火箭,而且它也是一个按钮!”时,我都会咯咯地笑,而且每个人都是如此。该死。时间。【参考方案3】:
根据提问者对如何使用它的说明(采用元组的代码),我将提出这个建议。
//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...);
;
【讨论】:
+1 太好了,第一个位让我可以随机访问一系列这样的向量。【参考方案4】:你可以使用类似的东西:
#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;
;
要进行迭代,请创建一个类似于https://***.com/a/20272955/2684539 中的“IndexIterator”
要连接其他向量,您必须创建另一个 ToVBackedVoT
,就像 std::tuple_cat
为 std::tuple
所做的那样
【讨论】:
为什么在模板参数中使用int
?
int...Is
用于将std::get<Is>(data)
解包为std::get<0>(data), .., std::get<N - 1>(data)
。 gen_seq<sizeof...(Ts)>
是 seq<0, 1, 2, .., sizeof...(Ts) - 1>
。这是一种迭代元组索引的方法。
哇,您正在修改所有答案以使它们与 C++1y 兼容? o.O
既然它可能需要一段时间才能使用,为什么不同时提及index_sequence
的替代品?
@leewangzhong:我不使用std::index_sequence
(因为它在 C++11 中不可用),我实现了一个类似的同名自定义版本以简化向 C++1y 的过渡.【参考方案5】:
转换为向量的 std::tuple (vector::iterators):
#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> > > >
【讨论】:
我在您的解决方案中看不到这对 OP 有何帮助。而你的concat_operation
似乎比需要的更复杂。 template <template <typename> class Operator, typename... Ts> struct concat_operation typedef std::tuple<typename Operator<Ts>::type...> type; ;
似乎足够了。
你的concat_operation
好像也不能和std::tuple
一起使用。【参考方案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-style functional library "fn" 的一部分,并使用可变参数模板实现。
目前由于库的设计原因,它不支持通过适配器修改原始范围的值(它旨在用于非可变范围,如函数式编程)。
简要说明说明如何做到这一点:zip(...)
返回一个实现了begin()
和end()
的适配器对象,返回一个迭代器对象。迭代器将迭代器的元组保存到包装范围。递增迭代器会递增所有包装的迭代器(这是使用索引列表实现的,并将递增表达式解包为一系列表达式:++std::get<I>(iterators)...
)。取消引用迭代器将递减所有包装的迭代器并将其传递给std::make_tuple
(这也实现为解包表达式*std::get<I>(iterators)...
)。
附:它的实现基于来自这个问题的答案的许多想法。
【讨论】:
您的示例代码不会导致从元组到三个变量的不必要赋值吗? @einpoklum 是的,但我不知道如何方便地使用参考。您可以执行int &x1 = get<0>(tuple);
,但这看起来很复杂,并且您可能会在索引中引入错误,尤其是在您更改元组/zip 时。可悲的是,您不能这样做int &x1; ...; tie(x1) = ...
,因为这会使引用未初始化,并且分配不会“重新定位”引用。如果 C++ 有“原生”元组(他们考虑过),我敢肯定像 for(auto & (x1,x2,x3) : zip(v1,v2,v3))
这样的东西是可能的。以上是关于如何使用 C++11 可变参数模板来定义由向量元组支持的元组向量?的主要内容,如果未能解决你的问题,请参考以下文章