【问题标题】:Implement STL functions in variadic template在可变参数模板中实现 STL 函数
【发布时间】:2017-12-24 10:44:53
【问题描述】:

我一直在从事一个小项目,以快速了解可变参数模板。我实现了一个小的多维数组。我现在想定义一个对给定位置的最近邻居进行操作的函数——有没有一种优雅的方法来检索我的数组中给定位置的邻居的值?

template<class T, size_t size, size_t... sizes>
struct MArr {
    typedef std::array<typename MArr<T, sizes...>::type, size> type;

    std::array<MArr<T, sizes...>,size> data;

    MArr<T, sizes...>& operator[](int i) {
        return data[i];
    }

};

template<class T, size_t size>
struct MArr<T, size> {
    typedef std::array<T, size> type;
    type data;

    T& operator[](int i) {
       return data[i];
    }

};

附录:我有点清楚如何通过使用递归来循环所有元素,例如将任意函数应用于超暗淡。整数数组:

template <typename T, size_t size>
void func(MArr<T, size>& arr, std::function<void(int &)> f) {
    for (int i = 0; i < size; i++) {
       f(arr[i]);
    }
}


template <typename T, size_t size0, size_t size1, size_t ...sizes>
void func(MArr<T, size0, size1, sizes...>& arr, std::function<void(int &)> f) {
    for (int i =0; i < size0; i++) {
        func<T, size1, sizes...>(arr[i], f);
    }
}

我很好奇我将如何生成类似func(arr[i+1,j,k,…],arr[i-1,j,k,…],arr[i,j+1,k,…],arr[i,j-1,k,…],…) 和给定函数(比如添加相应的元素)。正如我所说,可变参数模板很新,我觉得我还没有正确的心态......

【问题讨论】:

  • 那些是直接邻居还是对角线?即,一个 2D 单元有 4 个还是 8 个?
  • 直接邻居 -- 所以在 1D -> 2, 2D -> 4, 3D -> 6 ...
  • 在我看来func 基本合适。您需要做的就是删除循环,稍微调整索引并添加一个标志,如果您已经移动了 1 步。
  • 你的心态似乎很到位。有一些小问题,例如使用typedef 而不是using 和不必要地复制std::function,但你明白了它的要点。可能值得在 codereview.stackexchange.com 上发布,一旦它完成了您想要的操作。
  • @nwp,感谢您的评论。由于索引的长度可变,我不确定如何定义特定函数。你能再扩大一点吗?基本上我希望能够选择某个元素,然后在其所有邻居上调用一个函数......我认为如果我有固定数量的索引会相当容易,但在这种特殊情况下,我似乎有点迷失…

标签: c++ templates metaprogramming variadic-templates variadic-functions


【解决方案1】:

您可以执行类似的操作(代码使用 C++17 中的折叠表达式,但可以用 C++11 编写):

template <std::size_t I, typename F, std::size_t ... Is, typename Tuple>
void helper(F&& f, std::index_sequence<Is...>, const Tuple& t)
{
    f((std::get<Is>(t) - (Is == I))...);
    f((std::get<Is>(t) + (Is == I))...);
}

template <typename F, std::size_t ... Is, typename Tuple>
void helper(F&& f, std::index_sequence<Is...> Seq, const Tuple& t)
{
    (helper<Is>(std::forward<F>(f), Seq, t), ...);
}

template <typename F, typename ... Ts>
void apply_to_neighboor(F&& f, Ts... indexes)
{
    helper(std::forward<F>(f), std::index_sequence_for<Ts...>(), std::tie(indexes...));
}

Demo

如果要检索所有邻居,可以将上面的代码更改为:

template <std::size_t I, std::size_t ... Is, typename Tuple>
auto helper1(std::index_sequence<Is...>, const Tuple& t)
{
    return std::make_pair(std::make_tuple((std::get<Is>(t) - (Is == I))...),
                          std::make_tuple((std::get<Is>(t) + (Is == I))...));
}

template <std::size_t ... Is, typename Tuple>
auto helper(std::index_sequence<Is...> Seq, const Tuple& t)
{
    return std::tuple_cat(helper1<Is>(Seq, t)...);
}

template <typename F, typename ... Ts>
void apply_to_neighboor(F&& f, Ts... indexes)
{
    std::apply(std::forward<F>(f),
               helper(std::index_sequence_for<Ts...>(), std::tie(indexes...)));
}

Demo

【讨论】:

  • 非常聪明的方式依次选择每个维度!
  • 这很有趣,但这并不能真正回答 OP 的问题,即对函数 func 进行单次调用,该函数接受任意数量的参数。
  • @Holt:我希望添加的版本能填补你注意到的空白。
【解决方案2】:

这是一个可能的实现,它依赖于您的 MArr 提供自定义下标运算符:

// Subscript using an array of size_t
T operator[](const size_t (&idx)[sizeof... (sizes) + 1]) const;

思路如下:

  1. 您生成从02 * D * D - 1 的序列(其中D 是维数)。
  2. 使用此序列,您可以在单个一维数组中生成所有索引x + 1yzx - 1yz、...、x、@ 987654334@、z + 1xyz - 1
  3. 您将这个巨大的数组视为2 * D 数组D 值(size_t [2 * D][D]) 的数组。
  4. 您可以使用这个数组来索引您的多维数组,并使用新定义的下标运算符。

下面是实现:

template <class T, std::size_t... Sizes, class F, class... Is,
          std::size_t... IsP2N, std::size_t... IsP2NN>
auto around_impl(MArr<T, Sizes...> const& arr,
                 std::index_sequence<IsP2N...>,
                 std::index_sequence<IsP2NN...>,
                 F &&f, Is&&... pt) {
    const std::size_t pts[] = {(std::size_t)pt... };
    const std::size_t pts2[2 * sizeof...(Sizes)][sizeof...(Sizes)] = {
        (pts[IsP2NN % sizeof...(Sizes)] 
         + (1 - 2 * ((IsP2NN / sizeof...(Sizes)) % 2)) 
            * (IsP2NN % sizeof...(Sizes) == IsP2NN / (2 * sizeof...(Sizes))))...

    };
    return f(arr[pts2[IsP2N]]... );
}

template <class T, std::size_t... Sizes, class F, class... Is>
auto around(MArr<T, Sizes...> const& arr, F &&f, Is&&... pt) {
    return around_impl(arr,
                       std::make_index_sequence<2 * sizeof...(Sizes)>{},
                       std::make_index_sequence<2 * sizeof...(Sizes) * sizeof...(Sizes)>{},
                       std::forward<F>(f),
                       std::forward<Is>(pt)... );
}

关于实现的小笔记:

  • 本答案末尾解释了括号内发生的计算。
  • 从单个大括号括起来的列表初始化二维数组是有效的 C++(据我所知)。
  • 其中大部分已通过使用 -O3 的 clang 进行优化。

然后您可以调用:

// 3D array
MArr<int, 4, 4, 4> arr;

// Function to call:
auto myFun = [](int a, int b, int c, int d, int e, int f) { 
    return a + b + c + d + e + f;
};

// Call around:
around(arr, myFun, x, y, z);

打电话:

myFun(arr[x + 1, y, z], arr[x - 1, y, z], 
      arr[x, y + 1, z], arr[x, y - 1, z],
      arr[x, y, z + 1], arr[x, y, z - 1]);

解释:

(pts[IsP2NN % sizeof...(Sizes)] 
 + (1 - 2 * ((IsP2NN / sizeof...(Sizes)) % 2)) 
    * (IsP2NN % sizeof...(Sizes) == IsP2NN / (2 * sizeof...(Sizes))))...
  • IsP2NN0 变为 2 * D * D - 1
  • pts[...] 对应于xyzxyz、...等。
  • 1 - ... 零件生成 1, 1, 1, -1, -1, -1, 1, 1, @, 1, 1, 1, 1, 1 987654369@,等等……它在1-1的序列之间交替。
  • * (...)会在维度不匹配时将前一个序列作废,生成10010001、@ 987654381@、010等等……

以下是所发生情况的“矩阵”视图:

Is          0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17

Is % 3      0   1   2   0   1   2   0   1   2   0   1   2   0   1   2   0   1   2
Is / 3 % 2  0   0   0   1   1   1   0   0   0   1   1   1   0   0   0   1   1   1
Is / 6      0   0   0   0   0   0   1   1   1   1   1   1   2   2   2   2   2   2

pts  --     x   y   z   x   y   z   x   y   z   x   y   z   x   y   z   x   y   z
     --     1   0   0  -1   0   0   0   1   0   0  -1   0   0   0   1   0   0  -1
     --     1   0   0   1   0   0   0   1   0   0   1   0   0   0   1   0   0   1
---------------------------------------------------------------------------------
          x+1   y   z x-1   y   z   x y+1   z   x y-1   z   x   y z+1   x   y z-1

【讨论】:

  • 感谢您非常明确的回复。你能给我一个快速的指针,你将如何实现数组下标?我觉得这不可能递归,因为我一次只能访问数组的一个“层”?
  • @Faser 你不能直接递归地实现这个,但你可以回退到一个受保护的运算符,它将采用一个size_t*,这对于递归实现来说是微不足道的(只需在调用下一个之前增加指针函数)。
  • @Faser 类似this,但自定义聚合类型可能会更好。
【解决方案3】:

首先我要创建一个索引类型。

template<std::size_t N>
using index=std::array<std::size_t, N>;

template<class T, size_t size, size_t... sizes>
struct MArr {
  using my_index=index<sizeof...(sizes)+1>;

和一个索引参考。使用gsl::span 或自己编写:

namespace utility {
  template<class It>
  struct range {
    It b,e;
    It begin()const{return b;}
    It end()const{return e;}
    std::size_t size()const{return std::distance(begin(),end());}
    bool empty()const{return begin()==end();}

    using reference=typename std::iterator_traits<It>::reference;

    reference front()const{return *begin();}
    reference back()const{return *std::prev(end());}
  };
  template<class T>
  struct span:range<T*> {
    span(T* s, T*f):range<T*>{s,f}{}
    span():span(nullptr,nullptr){}
    span(T*s,std::size_t len):span(s,s+len){}
    T&operator[](std::size_t i)const{return this->begin()[i];}
    T* data()const{return this->begin();}
    span without_front(std::size_t n=1)const{ return {this->begin()+(std::min)(n,this->size()), end()}; }
    span without_back(std::size_t n=1)const{ return {this->begin(), end()-(std::min)(n,this->size())}; }
    span only_front(std::size_t n=1)const{ return {this->begin(),this->begin()+(std::min)(n,this->size())}; }
    span only_back(std::size_t n=1)const{ return {end()-(std::min)(n,this->size()),end()}; }
    span mid(std::size_t start, std::size_t len)const{ return without_front(start).only_front(len); }

    template< class U >
    using compatible=std::integral_constant<bool, std::is_convertible<U*,T*>{}&&(sizeof(U)==sizeof(T))>;
    template<class R>
    using compatible_range=compatible< std::decay_t<decltype( *std::declval<R>().data() )> >;
    template<class C,
      std::enable_if_t< compatible_range< C& >, bool> =true, // has .data() that returns good type
      std::enable_if_t< !std::is_same<span, std::decay_t<C>>{}, bool> =true // not own type
    >
    span(C&& c): span( c.data(), c.size() ){}
    template<std::size_t N>
    span( T(&arr)[N] ):span(arr, N){}
    // etc
  };
}

一旦你有了 span(或者像上面那样,或者来自 gsl),代码就会变得更加简洁:

template<std::size_t N>
using index=std::array<std::size_t, N>;

using index_cref=utility::span<std::size_t const>;
using index_ref=utility::span<std::size_t>;

template<class T, size_t size, size_t... sizes>
struct MArr {
  using my_index=index<sizeof...(sizes)+1>;
  T& operator[]( index_cref r ){ return data[r.front()][ r.without_front() ]; }

在一暗的情况下

T& operator[](index_cref r) {
   return data[r.front()];
}

一旦我们这样做了,您的问题就会变得更容易。

首先,我们重写您的 func 以迭代 index 的值。这可以按照您的方式完成,您可以在其中传入回调,或者您可以实现 next 来推进索引。

bool next_index( index_ref r, index_cref bounds ){
  if (r.empty()||bounds.empty()) return false;
  ++r.back();
  if (r.back()!=bounds.back()) return true;
  r.back()=0;
  return next_index( r.without_back(), bounds.without_back() );
}

现在迭代可以是这样的:

template<class MArr, class F>
void foreach_index( MArr const&, F&& f ){
  using index=typename MArr::index;
  index const bounds = MArr::bounds(); // todo: write
  index cur = {{0}};
  do {
    f(cur);
  } while( next_index(cur, bounds) );
}

比您的版本更干净、更简单、更高效(无类型擦除)。

最近的邻居可以很容易地写成index_ref

template<class F>
void foreach_neighbour( index_ref where, index_cref bounds, F&& f ){
  for(std::size_t i=0; i<(std::min)(where.size(),bounds.size());++i){
    if (where[i]){ where[i]--; f(where); where[i]++; }
    if (where[i]+1<bounds[i]) { where[i]++; f(where); where[i]--; }
  }
}

【讨论】:

  • 我不知道这一切有什么用。我看不到“更清洁、更简单”的说法的证据。由于用模板替换 std::function 而声称“更有效”是有道理的,但不需要你写的这个模板野兽。
  • (std::min)(n,this-&gt;size())而不是std::min(n,this-&gt;size())的目的是什么?
  • @nwp:我赌三片韭菜“这样 MSVC 的愚蠢的 min 宏就不会启动了”。
  • @nwp 更快是因为它没有大量使用递归,而不是因为std::function被模板替换了。
  • @nwp 最终“用户”代码更简单; span 是“库”或“词汇”代码。如果你有跨度,你会发现(根据我的经验)你会在任何地方使用它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-01
  • 2016-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多