ChatGPT解决这个技术问题 Extra ChatGPT

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

(注意:这个问题是关于不必指定元素的数量并且仍然允许直接初始化嵌套类型。)
This question 讨论了 C 数组的用途,例如 {3 }。在 his answer,@James Kanze 展示了 C 数组的最后据点之一,它具有独特的初始化特征:

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

我们不必指定元素的数量,万岁!现在使用来自 <iterator> (or your own variants) 的 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 东西:

可变参数模板

大小...

右值引用

完美转发

std::array,当然

统一初始化

使用统一初始化省略返回类型

类型推断(自动)

here 中有一个示例。

但是,正如@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#,为更复杂的支持提供语言支持。任何人都知道我们是否获得接口的正式语言支持???
@Downvoter:原因?
抱歉,您的问题中的 TMP 是什么意思?
@kevinarpe TMP 可能代表模板元编程。

P
Pavel Minaev

我能想到的最好的是:

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 两者都可以!
正是有了这样的答案,我才最了解 Bjarne 所说的感觉“像一门新语言”:) 可变参数模板、延迟返回说明符和类型推导一体化!
@Matthieu:现在从@DeadMG 的代码中添加右值引用、完美转发和统一初始化,并且您已经设置了许多新功能。 :>
@Cubbi:实际上,g++ 就在这里——在 C++0x 的聚合初始化中不允许缩小转换(但在 C++03 中允许——这是我不知道的重大变化!)。我将删除第二个 make_array 调用。
@Cubbi,是的,但这是一个显式转换 - 它还允许静默向下转换和其他类似的事情。这仍然可以通过使用 static_assert 和一些 TMP 来检测 Tail 何时不能隐式转换为 T ,然后使用 T(tail)...,但这留给读者练习 :)
e
einpoklum

我希望有一个简单的 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)... };
}

删除 return 语句中的 std::array<ret, sizeof...(T)>。这毫无意义地强制在 C++14 和 C++11 中存在数组类型上的移动构造函数(而不是从 T&& 构造)。
我喜欢 C++ 人所说的简单 :-)
C
Community

结合以前帖子中的一些想法,这是一个甚至适用于嵌套结构的解决方案(在 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]]]

(对于最后一个输出,我使用的是 pretty-printer。)

实际上,让我们提高这种结构的类型安全性。我们绝对需要所有类型都相同。一种方法是添加我在上面编辑过的静态断言。另一种方法是仅在类型相同时启用 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... 函数争论,也许可以做到,但不在核心语言中。


C
Community

使用尾随返回语法 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... operator 中列出

c++17版本

感谢 template argument deduction for class templates 提议,我们可以使用演绎指南来摆脱 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 标志编译


C++17 应该已经有一个演绎指南:en.cppreference.com/w/cpp/container/array/deduction_guides
Z
Zizheng Tai

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

1.无需依赖RVO

一些答案提到我们需要依赖 RVO 来返回构造的 array。那不是真的;我们可以利用 copy-list-initialization 来保证永远不会创建临时对象。所以而不是:

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

我们应该这样做:

return {{values}};

2. 使 make_array 成为 constexpr 函数

这允许我们创建编译时常量数组。

3. 无需检查所有参数是否属于同一类型

首先,如果它们不是,编译器无论如何都会发出警告或错误,因为列表初始化不允许缩小。其次,即使我们真的决定自己做 static_assert 的事情(也许是为了提供更好的错误消息),我们仍然应该比较参数的 decayed 类型而不是原始类型。例如,

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

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

如果我们只是static_assertabc 具有相同的类型,那么此检查将失败,但这可能不是我们所期望的。相反,我们应该比较它们的 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<std::decay_t<T>, …>,而不是返回一个 std::array<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, "!");

u
underscore_d

C++11 将支持(大多数?)标准容器的 this manner of initialization


但是,我认为 OP 不想指定数组的大小,但 size 是 std::array 的模板参数。所以你需要像 std::array n = {1,2,3,4,5}; 这样的东西
std::vector<> 不需要显式整数,我不确定为什么 std::array 会。
@Richard,因为 std::vector 具有动态大小,而 std::array 具有固定大小。看到这个:en.wikipedia.org/wiki/Array_(C%2B%2B)
@juanchopanza 但 {...} 语法意味着编译时常量范围,因此ctor应该能够推断出范围。
std::initializer_list::size 不是 constexpr 函数,因此不能这样使用。然而,libstdc++(GCC 附带的实现)计划推出其版本 constexpr
G
Gabriel Garcia

(@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;
}

我忽略了 std::array 元素的默认初始化。目前正在寻找修复。
@dyp我用你的代码更新了答案。如果您决定写下自己的答案,请告诉我,我会记下来。谢谢你。
不,还好。绑定一个临时数组来推断长度是你的想法,我没有检查我的代码是否可以编译。我认为这仍然是您的解决方案和答案,并进行了一些改进;)尽管有人可能会争辩说,像 Puppy 的回答中的可变参数 make_array 没有任何好处。
正确的。此外,模板不能从初始化列表中推断类型,这是问题的要求之一(嵌套大括号初始化)。
P
Peter

С++17 紧凑的实现。

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

3
303

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

我觉得尚未完全涵盖的一个特殊用途是,您希望获得一个用相当长的字符串文字初始化的 charstd::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;
    // ...
}

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

一个 hacky 解决方案是简单地将 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;
}

y
yasouser

如果 std::array 不是一个约束并且如果你有 Boost,那么看看 list_of()。这与您想要的 C 类型数组初始化不完全一样。但接近。


这是一个很好的。可以在此处找到有关使用它来分配嵌套结构的类似问题Using-assign-map-list-of-for-complex-types
Y
Yakk - Adam Nevraumont

创建阵列制造商类型。

它重载 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


“这些看起来不是好的解决方案。”这也是我要说的:p
B
Boann

对于结构数组,没有一个模板方法适合我,所以我制作了这个宏解决方案:

#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 内,因此表达式中的任何副作用只会正确发生一次。

玩得开心!