如何使用std :: array模拟C数组初始化“ int arr [] = {e1,e2,e3,…}”行为?


137

(注意:这个问题是关于不必指定元素的数量,而仍然允许嵌套类型直接初始化。)
这个问题讨论了C数组(如)所剩下的用法int arr[20];。@James Kanze 在回答中显示了C数组的最后据点之一,它具有独特的初始化特性:

int arr[] = { 1, 3, 3, 7, 0, 4, 2, 0, 3, 1, 4, 1, 5, 9 };

我们不必指定元素的数量,万岁!现在,使用C ++ 11函数std::beginstd::endfrom <iterator>或您自己的变体)对其进行迭代,您甚至无需考虑其大小。

现在,是否有任何(可能是TMP)方法可以达到相同目的std::array?允许使用宏使它看起来更好。:)

??? std_array = { "here", "be", "elements" };

编辑:中级版本,从各种答案编译而成,如下所示:

#include <array>
#include <utility>

template<class T, class... Tail, class Elem = typename std::decay<T>::type>
std::array<Elem,1+sizeof...(Tail)> make_array(T&& head, Tail&&... values)
{
  return { std::forward<T>(head), std::forward<Tail>(values)... };
}

// in code
auto std_array = make_array(1,2,3,4,5);

并使用各种酷C ++ 11的东西:

  • 可变参数模板
  • sizeof...
  • 右值引用
  • 完美的转发
  • std::array, 当然
  • 统一初始化
  • 通过统一初始化省略返回类型
  • 类型推断(auto

这里可以找到一个例子。

但是,正如@Johannes在对@Xaade的答案的评论中指出的那样,您无法使用此类函数初始化嵌套类型。例:

struct A{ int a; int b; };

// C syntax
A arr[] = { {1,2}, {3,4} };
// using std::array
??? std_array = { {1,2}, {3,4} };

同样,初始化器的数量限于实现所支持的函数和模板参数的数量。


可变参数方法。它不是初始化,更像是赋值,但是它是我能找到的最接近的。要进行初始化,您必须直接访问内存。
李·路维耶尔,

显然,C ++ 0x支持初始化程序语法。太棒了 就像变得更像C#,有了对更复杂支持的语言支持。有谁知道我们是否获得接口的正式语言支持???
李·路维耶尔,

10
@Downvoter:原因?
Xeo

1
抱歉,TMP您的问题是什么意思?
kevinarpe

1
@kevinarpe TMP可能代表模板元编程
BeeOnRope

Answers:


63

我能想到的最好的是:

template<class T, class... Tail>
auto make_array(T head, Tail... tail) -> std::array<T, 1 + sizeof...(Tail)>
{
     std::array<T, 1 + sizeof...(Tail)> a = { head, tail ... };
     return a;
}

auto a = make_array(1, 2, 3);

但是,这要求编译器执行NRVO,然后还跳过返回值的副本(这也是合法的,但不是必需的)。实际上,我希望任何C ++编译器都能对其进行优化,使其与直接初始化一样快。


gcc 4.6.0不允许第二个编译,抱怨缩小从double到value_type的转换,但是clang ++ 2.9两者都可以!
Cubbi

20
有了这样的答案,我最能理解Bjarne所说的“感觉像一种新语言” :)可变参数模板,延迟返回说明符和类型归纳功能都是一体的!
Matthieu M.

@Matthieu:现在,从@DeadMG的代码中添加右值引用,完美的转发和统一的初始化,您就可以设置许多新功能。:>
Xeo

1
@Cubbi:实际上,g ++就在这里-在C ++ 0x的聚合初始化中不允许缩小转换(但在C ++ 03中是允许的-我不知道的重大更改!)。我将删除第二个make_array电话。
帕维尔·米纳夫

@Cubbi,是的,但这是一个显式转换-它还允许静默向下转换等操作,这仍然可以通过使用static_assert和一些TMP来检测何时Tail无法隐式转换为T,然后使用T(tail)...,作为读者的练习:)
帕维尔·米纳夫

39

我希望简单make_array

template<typename ret, typename... T> std::array<ret, sizeof...(T)> make_array(T&&... refs) {
    // return std::array<ret, sizeof...(T)>{ { std::forward<T>(refs)... } };
    return { std::forward<T>(refs)... };
}

1
取下std::array<ret, sizeof...(T)>return说法。这毫无意义地迫使数组类型上的move构造函数T&&在C ++ 14和C ++ 11中存在(与从-构造相反)。
Yakk-Adam Nevraumont

7
我喜欢C ++人士这么简单的
说法

20

结合以前的文章中的一些想法,这是一个甚至对嵌套结构也适用的解决方案(已在GCC4.6中进行了测试):

template <typename T, typename ...Args>
std::array<T, sizeof...(Args) + 1> make_array(T && t, Args &&... args)
{
  static_assert(all_same<T, Args...>::value, "make_array() requires all arguments to be of the same type."); // edited in
  return std::array<T, sizeof...(Args) + 1>{ std::forward<T>(t), std::forward<Args>(args)...};
}

奇怪的是,不能将返回值作为右值引用,这不适用于嵌套构造。无论如何,这是一个测试:

auto q = make_array(make_array(make_array(std::string("Cat1"), std::string("Dog1")), make_array(std::string("Mouse1"), std::string("Rat1"))),
                    make_array(make_array(std::string("Cat2"), std::string("Dog2")), make_array(std::string("Mouse2"), std::string("Rat2"))),
                    make_array(make_array(std::string("Cat3"), std::string("Dog3")), make_array(std::string("Mouse3"), std::string("Rat3"))),
                    make_array(make_array(std::string("Cat4"), std::string("Dog4")), make_array(std::string("Mouse4"), std::string("Rat4")))
                    );

std::cout << q << std::endl;
// produces: [[[Cat1, Dog1], [Mouse1, Rat1]], [[Cat2, Dog2], [Mouse2, Rat2]], [[Cat3, Dog3], [Mouse3, Rat3]], [[Cat4, Dog4], [Mouse4, Rat4]]]

(对于最后的输出,我正在使用我的漂亮打印机。)


实际上,让我们提高此构造的类型安全性。我们绝对需要所有类型都相同。一种方法是添加一个静态断言,该断言我已在上面进行了编辑。另一种方法是仅make_array在类型相同时启用,如下所示:

template <typename T, typename ...Args>
typename std::enable_if<all_same<T, Args...>::value, std::array<T, sizeof...(Args) + 1>>::type
make_array(T && t, Args &&... args)
{
  return std::array<T, sizeof...(Args) + 1> { std::forward<T>(t), std::forward<Args>(args)...};
}

无论哪种方式,您都需要可变参数all_same<Args...>类型特征。这是从归纳std::is_same<S, T>(注意,衰减是很重要的,允许混合TT&T const &等):

template <typename ...Args> struct all_same { static const bool value = false; };
template <typename S, typename T, typename ...Args> struct all_same<S, T, Args...>
{
  static const bool value = std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value && all_same<T, Args...>::value;
};
template <typename S, typename T> struct all_same<S, T>
{
  static const bool value = std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value;
};
template <typename T> struct all_same<T> { static const bool value = true; };

请注意,make_array()按临时副本返回,允许编译器(具有足够的优化标志!)将其视为右值或以其他方式优化,并且std::array是聚合类型,因此编译器可以自由选择最佳的构造方法。

最后,请注意,make_array设置初始值设定项时,您无法避免复制/移动构造。因此std::array<Foo,2> x{Foo(1), Foo(2)};没有复制/移动,但是auto x = make_array(Foo(1), Foo(2));在将参数转发到时有两个复制/移动make_array。我认为您无法对此进行改进,因为您无法将可变参数的初始化程序列表按词法传递给帮助程序推导类型和大小-如果预处理器具有sizeof...可变参数的函数,也许可以这样做,但不能在核心语言中。


13

使用尾随返回语法make_array可以进一步简化

#include <array>
#include <type_traits>
#include <utility>

template <typename... T>
auto make_array(T&&... t)
  -> std::array<std::common_type_t<T...>, sizeof...(t)>
{
  return {std::forward<T>(t)...};
}

int main()
{
  auto arr = make_array(1, 2, 3, 4, 5);
  return 0;
}

不幸的是,对于聚合类,它需要明确的类型说明

/*
struct Foo
{
  int a, b;
}; */

auto arr = make_array(Foo{1, 2}, Foo{3, 4}, Foo{5, 6});

实际上,此make_array实现在sizeof ...运算符中列出


c ++ 17版本

多亏了类模板提议的模板参数推导,我们可以使用推导指南摆脱make_array助手

#include <array>

namespace std
{
template <typename... T> array(T... t)
  -> array<std::common_type_t<T...>, sizeof...(t)>;
}

int main()
{
  std::array a{1, 2, 3, 4};
  return 0; 
}

-std=c++1z在x86-64 gcc 7.0下用flag 编译


6
C ++ 17应该已经为此提供了一个演绎指南:en.cppreference.com/w/cpp/container/array/deduction_guides
underscore_d

6

我知道问这个问题已经有一段时间了,但是我觉得现有的答案仍然有一些缺点,所以我想提出一个稍微修改的版本。以下是我认为一些现有答案缺失的要点。


1.无需依赖RVO

一些答案提到我们需要依靠RVO返回构造的array。那是不对的。我们可以利用copy-list-initialization来确保永远不会创建临时文件。所以代替:

return std::array<Type, …>{values};

我们应该做:

return {{values}};

2. make_array一个constexpr功能

这使我们可以创建编译时常量数组。

3.无需检查所有参数是否具有相同的类型

首先,如果不是,则编译器将始终发出警告或错误,因为列表初始化不允许缩小。其次,即使我们真的决定做自己的static_assert事情(也许会提供更好的错误信息),我们仍然应该比较自变量的衰减类型而不是原始类型。例如,

volatile int a = 0;
const int& b = 1;
int&& c = 2;

auto arr = make_array<int>(a, b, c);  // Will this work?

如果我们只是static_assert荷兰国际集团认为abc有相同的类型,那么这个检查会失败,但是这可能不是我们所期待的。相反,我们应该比较它们的std::decay_t<T>类型(全部为ints)。

4.通过衰减转发的参数推导数组值类型

这类似于第3点。使用相同的代码段,但这次不显式指定值类型:

volatile int a = 0;
const int& b = 1;
int&& c = 2;

auto arr = make_array(a, b, c);  // Will this work?

我们可能想要创建一个array<int, 3>,但是现有答案中的实现可能都无法做到这一点。我们可以做的是,而不是返回a std::array<T, …>,而是返回astd::array<std::decay_t<T>, …>

这种方法有一个缺点:我们不能再返回arraycv限定值类型。但是在大多数情况下array<const int, …>,我们还是会使用而不是类似的东西const array<int, …>。需要权衡,但我认为这是合理的。C ++ 17 std::make_optional也采用这种方法:

template< class T > 
constexpr std::optional<std::decay_t<T>> make_optional( T&& value );

考虑到以上几点make_array,C ++ 14 的完整工作实现如下所示:

#include <array>
#include <type_traits>
#include <utility>

template<typename T, typename... Ts>
constexpr std::array<std::decay_t<T>, 1 + sizeof... (Ts)>
make_array(T&& t, Ts&&... ts)
    noexcept(noexcept(std::is_nothrow_constructible<
                std::array<std::decay_t<T>, 1 + sizeof... (Ts)>, T&&, Ts&&...
             >::value))

{
    return {{std::forward<T>(t), std::forward<Ts>(ts)...}};
}

template<typename T>
constexpr std::array<std::decay<T>_t, 0> make_array() noexcept
{
    return {};
}

用法:

constexpr auto arr = make_array(make_array(1, 2),
                                make_array(3, 4));
static_assert(arr[1][1] == 4, "!");

6

C ++ 11将支持对(大多数?)std容器进行这种初始化


1
但是,我认为OP不想指定数组的大小,但是size是std :: array的模板参数。因此,您需要像std :: array <unsigned int,5> n = {1,2,3,4,5};之类的东西。
juanchopanza

std::vector<>不需要显式整数,我不确定为什么std::array会这样。
理查德

@Richard,因为std :: vector具有动态大小,而std :: array具有固定大小。看到这个:en.wikipedia.org/wiki/Array_
C%2B%2B

@juanchopanza,但是{...}语法隐含了编译时常量范围,因此ctor应该能够推断出范围。
理查德

1
std::initializer_list::size不是constexpr函数,因此不能像这样使用。但是,有libstdc ++(GCC附带的实现)计划有其版本constexpr
Luc Danton

5

(@dyp解决方案)

注意:需要C ++ 14std::index_sequence)。尽管可以std::index_sequence在C ++ 11中实现。

#include <iostream>

// ---

#include <array>
#include <utility>

template <typename T>
using c_array = T[];

template<typename T, size_t N, size_t... Indices>
constexpr auto make_array(T (&&src)[N], std::index_sequence<Indices...>) {
    return std::array<T, N>{{ std::move(src[Indices])... }};
}

template<typename T, size_t N>
constexpr auto make_array(T (&&src)[N]) {
    return make_array(std::move(src), std::make_index_sequence<N>{});
}

// ---

struct Point { int x, y; };

std::ostream& operator<< (std::ostream& os, const Point& p) {
    return os << "(" << p.x << "," << p.y << ")";
}

int main() {
    auto xs = make_array(c_array<Point>{{1,2}, {3,4}, {5,6}, {7,8}});

    for (auto&& x : xs) {
        std::cout << x << std::endl;
    }

    return 0;
}

我忽略了std :: array元素的默认初始化。当前正在寻找修复程序。
加布里埃尔·加西亚

@dyp我用您的代码更新了答案。如果您决定写下自己的答案,请告诉我,我会拒绝我的。谢谢。
加布里埃尔·加西亚

1
不,还好。绑定一个临时数组以推断长度是您的主意,而我也没有检查我的代码是否可以编译。我认为这仍然是您的解决方案,并且做了一些改进;)尽管有人说make_array像Puppy的回答那样对可变参数没有好处,但有人可能会争辩说。
dyp 2014年

对。此外,模板无法从初始值设定项列表中推导类型,这是问题的要求之一(嵌套支撑初始化)。
加百利·加西亚

1

С++ 17紧凑的实现。

template <typename... T>
constexpr auto array_of(T&&... t) {
    return std::array{ static_cast<std::common_type_t<T...>>(t)... };
}


0

创建一个阵列制造商类型。

超载 operator,以生成一个表达式模板,该模板通过引用将每个元素链接到先前的元素。

添加一个 finish免费函数,该函数接受数组创建者并直接从引用链中生成数组。

语法应如下所示:

auto arr = finish( make_array<T>->* 1,2,3,4,5 );

它不允许{}像仅允许那样进行基础构造operator=。如果您愿意使用,=我们可以使其起作用:

auto arr = finish( make_array<T>= {1}={2}={3}={4}={5} );

要么

auto arr = finish( make_array<T>[{1}][{2}[]{3}][{4}][{5}] );

这些都不是好的解决方案。

使用变数将您限制为编译器施加的可变参数数量限制,并阻止{}子结构的递归使用。

最后,确实没有一个好的解决方案。

我做的是我写我的代码,因此它消耗都T[]std::array数据不可知 -它并不关心,我给它。有时这意味着我的转发代码必须小心地将[]数组std::array透明地转换为s。


1
“这些看起来不是很好的解决方案。” 我也想说的是:p
封顶
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.