【问题标题】:c++11 constexpr flatten list of std::array into arrayc++11 constexpr 将 std::array 列表展平为数组
【发布时间】:2014-07-31 20:37:47
【问题描述】:

我从 c++11 开始,constexpr 和模板元编程似乎是在微型微控制器上节省稀缺内存的好方法。

有没有办法编写模板来展平 constexpr 数组列表,什么 我需要的是一种方法:

constexpr std::array<int, 3> a1 = {1,2,3};
constexpr std::array<int, 2> a2 = {4,5};
constexpr auto a3 = make_flattened_array (a1,a2);

我使用 gcc 4.8.4 (arm-none-eabi),如果需要,可以使用 std=c++11 或 c++1y 选项进行编译。

【问题讨论】:

  • 您想加入这些阵列吗? this 之类的东西?
  • @Rapptz 为什么要回滚 C++1y 标签?正如 Luc Danton 的回答所表明的那样,使用index_sequence 机器,这种类型的东西变得容易得多。
  • @TemplateRex 这个问题与 C++1y 完全无关。严格来说,使用 index_sequence 并不是 C++1y,因为该技巧已在绝大多数 C++11 答案中使用。并不要求这些变成c++1y。
  • @Rapptz 标签不是互斥的,它可以更轻松地搜索使用 C++1y 技术的问答

标签: c++ arrays c++11 std constexpr


【解决方案1】:

注意 - 我对您的问题的理解如下:您想加入这两个数组并将结果展平为一个包含其元素串联的单个新数组。

您可以通过三个 C++11+ 概念来实现您的目标:

  1. Variadic templates
  2. constexpr expressions
  3. Parameter pack

您首先创建一个模板(一个空壳)来开始设计您的递归时尚列表展平功能:

template<unsigned N1, unsigned N2>
constexpr std::array<int, N1+N2> concat(const std::array<int, N1>& a1, const std::array<int, N2>& a2){
  // TODO
}

到目前为止一切顺利:constexpr 说明符将提示编译器在每次可以编译时评估该函数。

现在是有趣的部分:std::array 有(因为c++1y)constexpr overload for the operator[],这意味着你可以写类似的东西

template<unsigned N1, unsigned N2>
constexpr std::array<int, N1+N2> concat(const std::array<int, N1>& a1, const std::array<int, N2>& a2){
  return std::array<int,N1+N2>{a1[0],a1[1],a1[2],a2[0],a2[1]};
}

(注意aggregate-initialization从一系列整数值初始化对象)

显然,手动硬编码对两个数组的值的所有索引访问并不比仅仅声明连接数组本身更好。将拯救这一天的概念如下:Parameter Packs。模板参数包是接受 0 个或多个模板参数的模板参数。具有至少一个参数包的模板称为可变参数模板。

很酷的是能够将参数包扩展到指定位置,例如:

#include <iostream>
#include <array>

template<unsigned... Num>
std::array<int, 5> function(const std::array<int,5>& source) {
    return std::array<int,5>{source[Num]...};
}


int main() {
    std::array<int,5> source{7,8,9,10,11};
    std::array<int,5> res = function<0,1,2,3,4>(source);

    for(int i=0; i<res.size(); ++i)
        std::cout << res[i] << " "; // 7 8 9 10 11

    return 0;
}

所以我们现在唯一需要的是能够在编译时生成“索引系列”,例如

std::array<int,5> res = function<0,1,2,3,4>(source);
                                 ^ ^ ^ ^ ^

在这一点上,我们可以再次利用参数包和继承机制:这个想法是有一个深度嵌套的 derived : base : other_base : another_base : ... 类层次结构,它将索引“累积”到参数包中并终止索引为0时的“递归”。如果你没看懂上一句,别着急,看看下面的例子:

std::array<int, 3> a1{42,26,77};

// goal: having "Is" = {0,1,2} i.e. a1's valid indices
template<unsigned... Is> struct seq;

我们可以通过以下方式生成索引序列:

template<unsigned N, unsigned... Is>
struct gen_seq : gen_seq<N-1, Is...>{}; // each time decrement the index and go on
template<unsigned... Is>
struct gen_seq<0 /*stops the recursion*/, Is...> : /* generate the sequence */seq<Is...>{};

std::array<int, 3> a1{42,26,77};
gen_seq<3>{};

无论如何还是缺少一些东西:上面的代码将以 gen_seq 开头并实例化指定的模板,该模板将实例化 gen_seq 作为其将实例化 gen_seq 作为它的基类,它将实例化 gen_seq 作为它的基类,它将实例化 seq 作为最终序列。

序列是'(nothing)',有问题..

为了将索引“累积”到参数包中,您需要在每次递归时将减少的索引“添加”到参数包:

template<unsigned N, unsigned... Is>
struct gen_seq : gen_seq<N-1, /*This copy goes into the parameter pack*/ N-1, Is...>{};

template<unsigned... Is>
struct gen_seq<0 /*Stops the recursion*/, Is...> : /*Generate the sequence*/seq<Is...>{};
template<unsigned... Is> struct seq{};

// Using '/' to denote (nothing)
gen_seq<3,/> : gen_seq<2, 2,/> : gen_seq<1,  1,2,/> : gen_seq<0, 0,1,2,/> : seq<0,1,2,/> .

所以现在我们能够将所有片段重新收集在一起并生成两个索引序列:一个用于第一个数组,一个用于第二个数组,并将它们连接在一起形成一个新的返回数组,该数组将保存连接和展平的联合的两个数组(就像将它们附加在一起)。

此时,以下代码应该很容易理解:

#include <iostream>
#include <array>

template<unsigned... Is> struct seq{};
template<unsigned N, unsigned... Is>
struct gen_seq : gen_seq<N-1, N-1, Is...>{};
template<unsigned... Is>
struct gen_seq<0, Is...> : seq<Is...>{};

template<unsigned N1, unsigned... I1, unsigned N2, unsigned... I2>
// Expansion pack
constexpr std::array<int, N1+N2> concat(const std::array<int, N1>& a1, const std::array<int, N2>& a2, seq<I1...>, seq<I2...>){
  return { a1[I1]..., a2[I2]... };
}

template<unsigned N1, unsigned N2>
// Initializer for the recursion
constexpr std::array<int, N1+N2> concat(const std::array<int, N1>& a1, const std::array<int, N2>& a2){
  return concat(a1, a2, gen_seq<N1>{}, gen_seq<N2>{});
}

int main() {
    constexpr std::array<int, 3> a1 = {1,2,3};
    constexpr std::array<int, 2> a2 = {4,5};

    constexpr std::array<int,5> res = concat(a1,a2);
    for(int i=0; i<res.size(); ++i)
        std::cout << res[i] << " "; // 1 2 3 4 5

    return 0;
}

http://ideone.com/HeLLDm


参考资料:

https://stackoverflow.com/a/13294458/1938163

http://en.cppreference.com/

http://en.wikipedia.org

【讨论】:

  • 非常感谢您的回答!我终于理解了包装序列,在阅读了我能找到的所有参考资料后,我仍然无法理解。但问题是:为什么我们同时使用 I1 和 I2 ?我们不能重复使用同一个包,因为两个扩展都是独立的吗?
  • @Quentin 两个索引系列可能不同,这就是我们使用其中两个的原因
  • 现在我觉得自己很笨。嗯,我想该睡觉了。再次感谢!
  • 好吧,还没有看到 + :) 在 C++1y 中,无论如何我们都会在 StdLib 中有 integer_sequence 等,我们可以只使用一个循环。
  • 请注意,C++1y 整数序列将能够以对数深度(或更好)构建序列。上面的递归继承技巧是线性模板实例化深度:因此大于几百个的序列将导致编译器抱怨和失败。在模板元编程中,递归深度要最小化。
【解决方案2】:

使用 C++1y,实现可能(尽管不是必需的)允许 std::tuple_cat 使用任何类似元组的类型,而不仅仅是 std::tuple&lt;T...&gt;。在我们的例子中,std::array&lt;T, N&gt; 就是这样一种类型。所以我们可以尝试:

constexpr std::array<int, 3> a1 = {1, 2, 3};
constexpr std::array<int, 2> a2 = {4, 5};
constexpr auto a3 = std::tuple_cat(a1, a2);
// note:

// not possible
// constexpr auto e = a3[3]

// instead
constexpr auto e = std::get<3>(a3);

尽管如此,调用std::tuple_cat 的结果是一个元组,而不是一个数组。然后可以将std::tuple&lt;T, T,… , T&gt; 转换为std::array&lt;T, N&gt;:

template<
    typename Tuple,
    typename VTuple = std::remove_reference_t<Tuple>,
    std::size_t... Indices
>
constexpr std::array<
    std::common_type_t<std::tuple_element_t<Indices, VTuple>...>,
    sizeof...(Indices)
>
to_array(Tuple&& tuple, std::index_sequence<Indices...>)
{
    return { std::get<Indices>(std::forward<Tuple>(tuple))... };
}

template<typename Tuple, typename VTuple = std::remove_reference_t<Tuple>>
constexpr decltype(auto) to_array(Tuple&& tuple)
{
    return to_array(
        std::forward<Tuple>(tuple),
        std::make_index_sequence<std::tuple_size<VTuple>::value> {} );
}

(事实证明,只要元组元素类型兼容,to_array 实现就可以将任何类元组转换为数组。)

这里是a live example for GCC 4.8,填补了一些尚不支持的 C++1y 特性。

【讨论】:

    【解决方案3】:

    Luc 的帖子回答了这个问题。
    但为了好玩,这里有一个没有模板元编程的 C++14 解决方案,只是纯 constexpr。

    不过有一个问题,一年多前,广义 constexpr 被选为标准核心语言,但 STL 仍未更新......

    作为一个实验,打开标题&lt;array&gt; 并为非常量运算符[]添加一个明显缺失的 constexpr

    constexpr reference operator[](size_type n);
    

    同时打开&lt;numeric&gt; 并将std::accumulate 变成一个constexpr 函数

    template <class InputIterator, class T>
    constexpr T accumulate(InputIterator first, InputIterator last, T init);
    

    现在我们可以这样做了:

    #include <iostream>
    #include <array>
    #include <numeric>
    
    template <typename T, size_t... sz>
    constexpr auto make_flattened_array(std::array<T, sz>... ar)
    {
       constexpr size_t NB_ARRAY = sizeof...(ar);
    
       T* datas[NB_ARRAY] = {&ar[0]...};
       constexpr size_t lengths[NB_ARRAY] = {ar.size()...};
    
       constexpr size_t FLATLENGTH = std::accumulate(lengths, lengths + NB_ARRAY, 0);
    
       std::array<T, FLATLENGTH> flat_a = {0};
    
       int index = 0;
       for(int i = 0; i < NB_ARRAY; i++)
       {
          for(int j = 0; j < lengths[i]; j++)
          {
             flat_a[index] = datas[i][j];
             index++;
          }
       }
    
       return flat_a;
    }
    
    int main()
    {
      constexpr std::array<int, 3> a1 = {1,2,3};
      constexpr std::array<int, 2> a2 = {4,5};
      constexpr std::array<int, 4> a3 = {6,7,8,9};
    
      constexpr auto a = make_flattened_array(a1, a2, a3);
    
      for(int i = 0; i < a.size(); i++)
         std::cout << a[i] << std::endl;
    }
    

    (在clang trunk上编译运行)

    【讨论】:

    • 不错的实现,它以数组列表作为参数,无疑比模板元编程解决方案更容易阅读和理解。不幸的是,即使在 4.9 中,gcc 也没有推广 constexpr 支持,而且我不确定它是否计划用于 4.10,目前,它还没有完成,甚至还没有进行中。
    • 已经在 4.9 和 7.3 之间的某个时候更新了 constexpr 元素访问,但是 std::accumulate 仍然不是 constexpr,所以这个答案的用处仍然受到要求修改 的限制。但是,如果您愿意将编译器版本提升到 C++17,则可以使用折叠表达式而不是修改 std::accumulate:constexpr FLATLENGTH = ( sz + ... );。
    【解决方案4】:

    另一种方法是使用expression templates。它不复制数组。

    草图:

    #include <array>
    
    template<class L, class R>
    struct AddOp
    {
        L const& l_;
        R const& r_;
    
        typedef typename L::value_type value_type;
    
        AddOp operator=(AddOp const&) = delete;
    
        constexpr value_type const& operator[](size_t idx) const {
            return idx < l_.size() ? l_[idx] : r_[idx - l_.size()];
        }
    
        constexpr std::size_t size() const {
            return l_.size() + r_.size();
        }
    
        // Implement the rest of std::array<> interface as needed.
    };
    
    template<class L, class R>
    constexpr AddOp<L, R> make_flattened_array(L const& l, R const& r) {
        return {l, r};
    }
    
    constexpr std::array<int, 3> a1 = {1,2,3};
    constexpr std::array<int, 2> a2 = {4,5};
    constexpr std::array<int, 2> a3 = {6};
    constexpr auto a4 = make_flattened_array(a1,a2);
    constexpr auto a5 = make_flattened_array(a4,a3);
    
    int main() {
        constexpr auto x = a5[1];
        constexpr auto y = a5[4];
        constexpr auto z = a5[5];
    }
    

    【讨论】:

    • 在微控制器上进行裸编程,.text 段写入闪存,而不是内存,因此静态 const 变量写入闪存,所以复制不是问题。我需要的是将整个数组保存在 dma 引擎的连续内存中,它将读取该内存,因此您的解决方案将不起作用,但无论如何,我已经学习了另一种 c++ 功能!
    猜你喜欢
    • 2012-05-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多