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

145

(注意:此问题涉及无需指定元素数量,仍允许直接初始化嵌套类型。)
这个问题讨论了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 :: end或您自己的变量)从<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新特性:

  • Variadic Templates
  • sizeof...
  • rvalue引用
  • 完美转发
  • 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#一样,具有更复杂的支持的语言支持。有人知道我们是否会获得正式的语言支持接口吗? - Lee Louviere
C++0x的初始化语法答案去哪了? - Lee Louviere
11
@Downvoter: 原因是什么? - Xeo
1
抱歉,请问你的问题中“TMP”的意思是什么? - kevinarpe
1
@kevinarpe TMP可能代表“模板元编程”。 - BeeOnRope
显示剩余16条评论
13个回答

65
我能想到的最好的办法是:
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
22
像这样的回答让我更理解Bjarne所说的感觉"像是一种新语言" :) 可变参数模板、后置返回类型和类型推导三位一体! - Matthieu M.
@Matthieu:现在将@DeadMG的代码中的rvalue引用、完美转发和统一初始化加入进来,你就会得到许多新功能。:> - Xeo
1
@Cubbi:实际上,g++是正确的——在C++0x中,聚合初始化不允许缩小转换(但在C++03中允许——这是我不知道的一个破坏性变化!)。我将删除第二个make_array调用。 - Pavel Minaev
1
在C++17中,对函数进行轻微更改即可保证省略。删除变量a并直接返回即可。在C++11中,可以通过在返回值中进行大小计算,然后执行return {{ blah }}来保证省略。 - Yakk - Adam Nevraumont
显示剩余2条评论

38

我期望有一个简单的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
return语句中删除std::array<ret, sizeof...(T)>。这样做毫无意义,只会强制存在一个移动构造函数于数组类型上(与在C++14和C++11中从T&&构造不同)。 - Yakk - Adam Nevraumont
19
我很喜欢 C++ 开发者把那个简单的东西称作这样 :-)。 - Ciro Santilli OurBigBook.com

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() 返回一个临时副本,编译器(使用足够的优化标志!)可以将其视为rvalue或以其他方式进行优化,并且std::array是一个聚合类型,因此编译器可以自由选择最佳的构造方法。
最后,请注意,当make_array设置初始化程序时,您无法避免复制/移动构造。因此,std::array<Foo,2> x{Foo(1), Foo(2)};没有复制/移动,但是auto x = make_array(Foo(1), Foo(2));有两个复制/移动,因为参数被转发到make_array。我认为您无法改进这一点,因为您无法在语法上将可变数量的初始化列表传递给帮助程序并推断类型和大小--如果预处理器对于可变参数有一个sizeof...函数,则可能可以完成,但不能在核心语言中完成。

17
使用尾随返回语法,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});

EDIT 不再相关: 事实上,这个make_array实现在sizeof...操作符中列出


根据[namespace.std]/4.4的规定,下面的代码引入了未定义的行为

4.4 如果C++程序声明了任何标准库类模板的推导指南,则其行为是未定义的。

# 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; 
}

使用x86-64 gcc 7.0编译,标志为-std=c++1z


10
C++17应该已经为这个做了一个推断指南:http://en.cppreference.com/w/cpp/container/array/deduction_guides - underscore_d
这样的实现不会因为扩展标准命名空间而导致未定义的行为吗? - Alvov1
1
@Alvov1,根据标准中的[namespace.std]/3.4,是可以的。 - wiped

9

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


1. 不需要依赖RVO

一些答案提到我们需要依靠RVO来返回构造的array。这并不正确;我们可以利用复制列表初始化来保证永远不会创建临时对象。因此,我们可以使用以下代码:

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> 类型(它们都是 int)。

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

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

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

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

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

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

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

考虑到上述要点,在C++14中实现make_array的完整工作代码如下:
#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, "!");

5

(@dyp 提供的解决方案)

注意:需要使用C++14std::index_sequence)。虽然可以在C++11中实现std::index_sequence

#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元素的默认初始化。目前正在寻找解决方法。 - Gabriel Garcia
@dyp 我已经更新了答案,加入了你的代码。如果你决定自己写一个答案,请告诉我,我会删除我的答案。谢谢。 - Gabriel Garcia
1
不,没关系。将临时数组绑定以推导长度是你的想法,而我甚至没有检查我的代码是否编译。我认为这仍然是你的解决方案和答案,只需要进行一些改进 ;) 尽管如此,有人可能会认为像Puppy的答案中的可变参数make_array并没有任何好处。 - dyp
没错。此外,模板不能从初始化列表中推断类型,这是问题要求之一(嵌套花括号初始化)。 - Gabriel Garcia

5

1
然而,我认为 OP 不想指定数组的大小,但是 std::array 的大小是一个模板参数。因此,你需要像 std::array<unsigned int,5> n = {1,2,3,4,5}; 这样的东西。 - juanchopanza
std::vector<> 不需要显式整数,而我不确定为什么 std::array 需要。 - Richard
@Richard,因为std::vector具有动态大小,而std::array具有固定大小。请参考这个链接:http://en.wikipedia.org/wiki/Array_(C%2B%2B) - juanchopanza
我找不到initializer_list<>规范,但我怀疑像这样的东西可能会起作用:std::initializer_list<int> init_list = {1,2,3}; std::array<int, init_list.size()> my_array(init_list); - Richard
1
std::initializer_list::size 不是一个 constexpr 函数,因此不能像这样使用。不过,libstdc++(GCC 附带的实现)有计划将它们的版本变为 constexpr - Luc Danton
显示剩余3条评论

2

C++20更新: 尽管有一些提供所需功能的优秀答案(例如使用std::index_sequenceGabriel Garcia's answer),但我添加这个答案是因为截至C++20最简单的方法没有被提及: 只需使用std::to_array()。使用OP的最后一个结构数组示例:

struct A{ int a; int b; };
// C syntax
A arr[] = { {1,2}, {3,4} };
// using std::array
auto std_array = std::to_array<A>({ {1,2}, {3,4} });

1

虽然这个答案更多地是针对this的问题,但那个问题被标记为此问题的重复。因此,这个答案在这里发布。

我觉得没有完全涵盖的一个特定用途是:当您想要获得一个使用相当长的字符串文字初始化的std::arraychar,但不希望炸毁封闭函数时。有几种方法可以解决这个问题。

以下方法可行,但需要我们明确指定字符串文字的大小。这就是我们要避免的:

auto const arr = std::array<char const, 12>{"some string"};

你可能期望以下代码会产生所需的结果:

auto const arr = std::array{"some string"};

由于模板推导,初始化数组时无需明确指定数组大小。但是,这样做行不通,因为现在的arr类型为std::array<const char*, 1>
一个简洁的方法是为std::array编写一个新的推导指南。但请记住,某些其他代码可能依赖于std::array推导指南的默认行为。
namespace std {
    template<typename T, auto N>
    array(T (&)[N]) -> array<T, N>;
}

使用这个推导指导,std::array{"some string"}; 将会成为 std::array<const char, 12> 类型。现在可以使用在其他地方定义的字符串字面量初始化 arr,无需指定其大小。
namespace {
    constexpr auto some_string = std::array{"some string"};
}

auto func() {
    auto const arr = some_string;
    // ...
}

好的,但是如果我们需要一个可修改的缓冲区,并且想要使用字符串字面量初始化它而不指定其大小怎么办?

一个巧妙的解决方法是简单地将std::remove_cv类型特征应用于我们的新推断指南。这不被推荐,因为这会导致相当令人惊讶的结果。字符串字面量的类型是const char[],因此我们的推断指南应该尝试匹配它。

在这种情况下似乎需要一个辅助函数。使用constexpr说明符,可以在编译时执行以下函数:

#include <array>
#include <type_traits>

template<typename T, auto N>
constexpr auto make_buffer(T (&src)[N]) noexcept {
    auto tmp = std::array<std::remove_cv_t<T>, N>{};

    for (auto idx = decltype(N){}; idx < N; ++idx) {
        tmp[idx] = src[idx];
    }
    return tmp;
}

使得可以像这样初始化可修改的类似于 std::array 的缓冲区:
namespace {
    constexpr auto some_string = make_buffer("some string");
}

auto func() {
    auto buff = some_string;
    // ...
}

随着C++20的到来,辅助函数甚至可以被简化:

#include <algorithm>
#include <array>
#include <type_traits>

template<typename T, auto N>
constexpr auto make_buffer(T (&src)[N]) noexcept {
    std::array<std::remove_cv_t<T>, N> tmp;
    std::copy(std::begin(src), std::end(src), std::begin(tmp));
    return tmp;
}

1

С++17紧凑实现。

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

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