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

(注意: 这个问题是关于不必指定元素的数量,但仍然允许直接初始化嵌套类型。)
这个问题 讨论了像 int arr[20];这样的 C 数组的剩余用途。在 他的回答上,@James Kanze 展示了 C 数组的最后一个要点,它独特的初始化特性:

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

我们不需要指定元素的数量,万岁!现在使用来自 <iterator>(或者你自己的变种)的 C + + 11函数 std::beginstd::end迭代它,您甚至不需要考虑它的大小。

现在,是否有任何(可能的 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} };

此外,初始化器的数量限制为实现支持的函数和模板参数的数量。

14433 次浏览

如果 std: : array 不是约束,并且如果有 Boost,则查看 list_of()。这与您想要的 C 类型数组初始化不完全一样。但也差不多了。

我只能想到:

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 + + 编译器都能够优化它,使其与直接初始化一样快。

我以为是简单的 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)... };
}

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

结合以前文章中的一些想法,这里有一个甚至适用于嵌套构造的解决方案(在 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...>类型 trait。下面是从 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...函数,那么也许可以做到这一点,但是不能在核心语言中完成。

(解决方案@dyp)

注意: 需要 C + + 14(std::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;
}

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


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?

如果我们简单地认为 abc具有相同的类型,那么这个检查将会失败,但这可能不是我们所期望的。相反,我们应该比较它们的 std::decay_t<T>类型(都是 int)。

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>,但是现有答案中的实现可能都做不到这一点。我们可以做的是,不返回一个 std::array<T, …>,而返回一个 std::array<std::decay_t<T>, …>

这种方法有一个缺点: 我们不能再返回 cv 限定值类型的 array。但是大多数时候,我们会使用 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, "!");

创建数组创建器类型。

它重载 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}] );

这些看起来都不像是好的解决方案。

使用 varardics 限制了编译器对 varargs 数量的限制,并阻止了子结构对 {}的递归使用。

最后,真的没有一个好的解决办法。

我所做的就是编写代码,让它同时使用 T[]std::array数据 不可知论——它不在乎我给它提供哪些内容。有时,这意味着我的转发代码必须小心地将 []数组透明地转换成 std::array

使用结尾返回语法 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实现在 < a href = “ http://en.cpferences ence.com/w/cpp/language/sizeof...”rel = “ nofollow norefrer”> 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标志编译

+ + 17紧凑型实施。

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

虽然这个答案更多的是针对 这个问题,但是这个问题被标记为这个问题的重复。因此,这个答案张贴在这里。

我觉得还没有完全说明的一个特殊用法是,您希望获得用相当长的字符串字面值初始化的 charstd::array,但不希望获得包含函数 爆炸std::array。有几种方法可以解决这个问题。

下面的代码可以工作,但是需要我们显式地指定字符串文字的大小。这就是我们要避免的:

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[],所以我们的演绎指南应该尝试匹配它。

在这种情况下,似乎需要一个 helper 函数。通过使用 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,helper 函数甚至可以被简化:

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

对于结构数组,没有一种模板方法可以正常工作,所以我精心制作了这个宏解决方案:

#define make_array(T, ...) \
(std::array<T,sizeof((T[]){ __VA_ARGS__ })/sizeof(T)> \{\{ __VA_ARGS__ }})
auto a = make_array(int, 1, 2, 3);


struct Foo { int x, y; };


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

请注意,虽然宏将其数组参数展开两次,但第一次是在 sizeof 中,因此表达式中的任何副作用只会正确发生一次。

玩得开心!

C + + 20更新: 尽管有一些非常好的答案提供了所需的功能(比如使用 std::index_sequence加布里埃尔 · 加西亚的回答) ,但我之所以添加这个答案,是因为没有提到 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} });