使用std::vector进行std::array的初始化

6
假设我有一个编译时已知大小的std::vector,我想把它转换成std::array。我该怎么做?有没有标准函数可以实现这个功能?
到目前为止,我找到的最佳解决方案是:
template<class T, std::size_t N, class Indexable, std::size_t... Indices>
std::array<T, N> to_array_1(const Indexable& indexable, std::index_sequence<Indices...>) {
  return {{ indexable[Indices]... }};
}

template<class T, std::size_t N, class Indexable>
std::array<T, N> to_array(const Indexable& indexable) {
  return to_array_1<T, N>(indexable, std::make_index_sequence<N>());
}

std::array<Foo, 123> initFoos {
  std::vector<Foo> lst;
  for (unsigned i = 0; i < 123; ++i)
    lst.push_back(getNextFoo(lst));
  return to_array<Foo, 123>(lst); // passing lst.begin() works, too
}

该应用程序类似于使用非默认构造类型填充std :: array(无可变参数模板):我也有一个不是默认构造的类型,因此我需要在数组初始化时计算实际值。但与那个问题相反,对于我来说,这些值不仅仅是索引函数,还取决于前面的值。我可以使用循环更轻松地构建我的值,而不是一系列函数调用。因此,我在循环中构建元素并将它们放入向量中,然后我想使用该向量的最终状态来初始化数组。

上述内容似乎编译并正常工作,但也许有改进的方法。

  1. 也许我可以巧妙地利用一些标准库功能,这是我不知道的。
  2. 也许我可以以某种方式避免使用帮助函数。
  3. 也许我可以以某种方式制定这个问题,使其适用于元素的移动语义而不是上面所使用的复制语义。
  4. 也许我可以避免使用operator[]进行随机访问,而只使用前向迭代器语义,这样它也可以适用于std::setstd::forward_list作为输入。
  5. 也许我应该停止使用std::vector,而是使用C++17或一些等效实现中的std::array<std::optional<T>, N>来表达我的目标。

相关问题:


1
有趣的问题,但这感觉不对。为什么要生成数据然后再复制它们呢?这是由于您想将它们打包在std::array中的愿望所迫,但相比原始的std::vector,这有什么优势呢?两者都将数据保存在连续的内存块中,因此前者并没有比后者更具优势。唯一的区别是size是一个编译时变量,但无论如何都是如此。 - Walter
@Walter:问题很有价值。一方面,我的原始数据是std::set,所以我至少需要进行一次转换。而且我还使用了一个转换构造函数来执行元素的类型转换。因此,这不仅仅是保留std::vector的问题。虽然我可以使用它。主要的反对意见可能是我感觉编译器知道得越多,它就能优化得越好。实际上,我只处理24个元素,尽管这可能对于完全展开循环来说太多了,但它可以例如展开4个元素的组,因为它知道计数可被4整除。这只是一种感觉。 - MvG
1个回答

5
我建议:
template<typename T, typename Iter, std::size_t... Is>
constexpr auto to_array(Iter& iter, std::index_sequence<Is...>)
-> std::array<T, sizeof...(Is)> {
    return {{ ((void)Is, *iter++)... }};
}

template<std::size_t N, typename Iter,
         typename T = typename std::iterator_traits<Iter>::value_type>
constexpr auto to_array(Iter iter)
-> std::array<T, N> {
    return to_array<T>(iter, std::make_index_sequence<N>{});
}

这个函数通过迭代器推断元素类型,并将复制和移动的语义留给调用者 - 如果调用者想要移动,他们可以通过std::move_iterator等方式选择加入:

auto initFoos() {
    constexpr unsigned n{123};

    std::vector<Foo> lst;
    for (unsigned i{}; i != n; ++i) {
        lst.push_back(getNextFoo(lst));
    }

    // copy-init array elements
    return to_array<n>(lst.cbegin());

    // move-init array elements
    return to_array<n>(std::make_move_iterator(lst.begin()));
}

在线演示


编辑:如果想要覆盖推断的元素类型,如评论中所示,那么我建议:

template<typename T, typename Iter, std::size_t... Is>
constexpr auto to_array(Iter& iter, std::index_sequence<Is...>)
-> std::array<T, sizeof...(Is)> {
    return {{ ((void)Is, T(*iter++))... }};
}

template<std::size_t N, typename U = void, typename Iter,
         typename V = typename std::iterator_traits<Iter>::value_type,
         typename T = std::conditional_t<std::is_same<U, void>{}, V, U>>
constexpr auto to_array(Iter iter)
-> std::array<T, N> {
    return to_array<T>(iter, std::make_index_sequence<N>{});
}

这使得元素类型变成了可选项,但它成为第二个参数而不是第一个,因此使用方式应该像这样:to_array<N, Bar>(lst.begin())而不是to_array<Bar, N>(lst.begin())

1
多个 iter++ 表达式的求值顺序是什么? - Maxim Egorushkin
3
在花括号初始化列表中的所有扩展都保证从左到右执行。 - ildjarn
1
谢谢!这解决了我列表中的第三和第四点,而我之前并不知道移动迭代器。在我的实际应用中,我正在使用显式转换构造函数,使用Bar::Bar(const Foo&)vector<Foo> 初始化 array<Bar,N>,将来也许会用到Bar::Bar(Foo&&)。结果表明,你提供的代码不算是显式转换,而我原来的代码是。因此我在那里添加了一个T(…)构造函数调用。 - MvG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接