使用std::array相比C风格数组有哪些优势?

150

如果我想要创建一个非常简单的数组,例如:

int myArray[3] = {1,2,3};

我应该使用 std::array 吗?

std::array<int, 3> a = {{1, 2, 3}};

使用std::array相对于普通数组而言有什么优势?它在性能上更高吗?只是更容易进行复制/访问处理吗?


3
使用std::定义多维数组可能会很困难。 - goGud
33
不难,只是需要更冗长地表达。 - Mike Seymour
2
指针衰减、引用等等,C语言的数组有许多奇怪之处。在C语言的数组中,迭代器可能是一个指针,并且 for (auto i = ++std::begin(myArray); . . . 可能甚至无法编译(至少在clang 6下似乎基本类型的临时变量不可变)。 - Patrick Fromberg
1
初始化也有神奇的不同:struct Direction { int32_t dw; int32_t dh; };static const Direction DIRECTIONS[DIRECTIONS_COUNT] { { -1, 1}, {0,1}, {1,1} , { 1, 0 }, {1,-1}, {0,-1} , {-1,-1}, {-1,0} }; 可以编译通过。但是如果你改成一个带有相同初始化列表的 std::array<Direction,DIRECTIONS_COUNT>,突然间你会得到“太多的初始化器”错误。(VS 2019 Community with language = C++17) - BitTickler
2
@Marc.2377 std::array只是C风格数组的包装器。如果我没记错,在一些早期版本的C++中,你不能用单括号初始化它。然而,不要完全相信我的话,因为我没有经常使用它们。 - H-005
显示剩余2条评论
8个回答

171

使用std::array相比普通数组有哪些优势?

它具有友好的值语义,因此可以通过值传递到函数中或从函数中返回。其接口使得查找大小更加方便,并且可以与STL风格的基于迭代器的算法一起使用。

它的性能更高吗?

它应该完全相同。按定义,它是包含数组作为唯一成员的简单聚合。

只是更容易进行复制/访问吗?

是的。


65

std::array是一个非常薄的包装器,用于C风格数组,基本上定义为

template<typename T, size_t N>
struct array
{
    T _data[N];
    T& operator[](size_t);
    const T& operator[](size_t) const;
    // other member functions and typedefs
};

它是一个聚合类型,几乎可以像基本类型一样使用(即可以按值传递、赋值等,而标准 C 数组不能直接复制或分配到另一个数组)。您应该查看一些标准实现(从您喜欢的 IDE 跳转到定义,或直接打开<array>),这是 C++ 标准库中相当容易阅读和理解的一部分。


1
小修正,它在文档中被定义为结构体:https://en.cppreference.com/w/cpp/container/array - Overt_Agent
3
@Overt_Agent 谢谢,已经更正(尽管 class...{public:...} 与结构体具有相同的访问规则;) - vsoftco

37
< p >std::array 用作C数组的零开销包装器,为其提供了其他C++容器的“正常”值语义。

在您仍然可以享受额外功能的同时,不应该注意到任何运行时性能差异。

如果您有C++11或boost,则使用std::array而不是int []样式数组是一个好主意。


26

它的性能更好吗?

它应该完全相同。根据定义,它是一个简单的聚合体,只包含一个数组成员。

情况似乎更加复杂,因为 std::array 在特定平台上与 C 数组相比并不总是生成相同的汇编代码。

我在 godbolt 上测试了这种情况:

#include <array>
void test(double* const C, const double* const A,
          const double* const B, const size_t size) {
  for (size_t i = 0; i < size; i++) {
    //double arr[2] = {0.e0};//
    std::array<double, 2> arr = {0.e0};//different to double arr[2] for some compiler
    for (size_t j = 0; j < size; j++) {
      arr[0] += A[i] * B[j];
      arr[1] += A[j] * B[i];
    }
    C[i] += arr[0];
    C[i] += arr[1];
  }
}

GCCClang都为C数组版本和std::array版本生成相同的汇编代码。

然而,MSVCICPC为每个数组版本生成不同的汇编代码。(我测试了带有-Ofast-Os选项的ICPC19;MSVC带有-Ox-Os选项)

我不知道为什么会出现这种情况(我确实期望std::array和c-array完全一致)。也许采用了不同的优化策略。

额外说明:

ICPC似乎存在一个错误

#pragma simd 

在某些情况下使用C数组进行向量化时需要注意,因为C数组的代码会产生错误的输出;而std::array版本可以正常工作。

不幸的是,由于我是在优化一段相当复杂的代码时才发现这个问题的,所以目前我还没有最小可行示例。

等我确定我没有对C数组/std::array#pragma simd有误解后,我会向英特尔报告此问题。


2
这可以被视为编译器的错误吗? - OznOg
3
实际测试过的那个人,在得到赞同的答案底部。 - Katastic Voyage

13

std::array 具有值语义,而原始数组则没有。这意味着您可以复制 std::array 并将其视为基本值。您可以通过值或引用作为函数参数接收它们,并且可以通过值返回它们。

如果您从未复制过 std::array ,那么与原始数组相比,它们的性能没有差异。如果您需要进行复制,则 std::array 将执行正确操作,并应仍然提供相同的性能。


3
您可以使用std::arrayc数组获得相同的性能结果。 如果运行以下代码:
std::array<QPair<int, int>, 9> *m_array=new std::array<QPair<int, int>, 9>();
    QPair<int, int> *carr=new QPair<int, int>[10];
    QElapsedTimer timer;
    timer.start();
    for (int j=0; j<1000000000; j++)
    {

        for (int i=0; i<9; i++)
        {
            m_array->operator[](i).first=i+j;
            m_array->operator[](i).second=j-i;
        }
    }
    qDebug() << "std::array<QPair<int, int>" << timer.elapsed() << "milliseconds";
    timer.start();
    for (int j=0; j<1000000000; j++)
    {

        for (int i=0; i<9; i++)
        {
            carr[i].first=i+j;
            carr[i].second=j-i;
        }
    }
    qDebug() << "QPair<int, int> took" << timer.elapsed() << "milliseconds";
    return 0;

您将得到以下结果:

std::array<QPair<int, int> 5670 milliseconds
QPair<int, int> took 5638 milliseconds

迈克·西莫(Mike Seymour)是正确的,如果您可以使用std::array,您应该使用它。


2
std::array解决了C风格数组存在的许多问题。这主要不是关于性能,而是关于正确性和便利性。以下是std::array解决的C风格数组问题的列表。
1. 数组无法从函数中返回
这个问题特别令人烦恼。特别是对于像初始化查找表这样的任务,返回数组是很有用的:
constexpr auto lookup = [] {
    std::array<int, 128> result;
    // compute the contents
    return result;
};

2. 返回数组的假设语法是被诅咒的。
int get_array()[10];
auto get_array() -> int[10]; // workaround: trailing return types

根据C语言的声明语法,这是如何声明一个返回包含10个整数的数组的函数get_array的方式。 这种语法非常令人惊讶,也是为什么它很不可能在C语言中被标准化的原因之一。

3. 数组不能被赋值

int arr[] = {0, 1, 2, 3};
arr = something_else; // ill-formed

分配数组对于小数组(如对或三元组)来说是相当常见的。 这种感觉特别不一致,因为语法=可以用于初始化。
4. 数组不能被初始化为其他数组。
int data[] = {0, 1};
int copy[] = data;

这是另一个感觉任意的限制。更糟糕的是,复制初始化原则上对数组是有效的,只是不能使用另一个数组。
5. 使用"=="比较数组是一个常见的错误。
int arr[] = {0, 1, 2, 3};
arr == arr; // true, but doesn't compare contents, but is pointer comparison

这是一个常见的错误,这就是为什么在C++23中废弃了数组比较,并且很可能在C++26中被移除。
另请参阅为什么数组之间的==相等比较不起作用? 6. 数组会衰变为指针,从而产生令人惊讶的语义。
char arr[] = "hello ";
if (arr); // always true, even if array contains an empty string
+arr;     // OK, but what does it mean to apply unary plus to an array?!
arr + 1;  // OK, but result is not "hello 1", it is "ello "

列表还在继续。 数组衰变为指针的事实经常导致令人费解的行为。 大部分这种行为尚未被弃用。
7. 数组可能是可变长度数组(VLAs)。
int size = rand();
int vla[size]; // could be OK if the compiler supports VLAs as an extension
std::array<int, size> arr; // error, as expected

没有编译器警告(对于GCC/clang来说,使用-Wvla),我们可能会无意中创建一个VLA。这使得代码不可移植,因为并非每个编译器都支持VLA。

8. 函数参数中的数组类型调整是误导性的

void foo(int arr[4]) {            // equivalent to accepting a parameter of type int*
    sizeof(arr) / sizeof(arr[0]); // = sizeof(void*) / sizeof(int), most likely 2
}

void foo(std::array<int, 4> arr) {
    arr.size(); // 4, correct
}

在函数参数中,数组类型的参数被调整为指针类型。 这意味着sizeof(arr)无法正常工作,并且提供的大小[4]实际上是没有意义的。
这非常令人惊讶,在期望数组的地方使用指针与sizeof结合使用是C语言中最初级的错误之一。

9. 无法从函数参数中推断数组大小

template <std::size_t N>
void foo(int arr[N]); // N can't be deduced from the array parameter

template <std::size_t N>
void foo(int (&arr)[N]); // workaround with complicated syntax

这个问题是由于前面指针类型调整的结果。在参数中没有数组类型可以推导出N。需要一个解决方法,但这个解决方法并不美观。
10. 数组在通用代码中会导致特殊情况
由于前面提到的所有不规则性,数组在通用代码中需要特殊处理。 举几个例子: - std::swap 必须对数组进行重载,因为数组是不可移动的 - 数组不是类,所以需要很多自由函数,如std::sizestd::beginstd::empty等,它们对数组有特殊处理的情况
任何开发者如果在库中使用range.begin()而不是std::begin(range),就必须担心他们的代码会在用户使用C风格数组时出错。 当然,使用std::array并不能解决根本问题,但它意味着你永远不会遭受库开发者错误地假设.begin()总是有效的问题。
此外,这些无数的特例仍然无法涵盖所有情况。你可以使用std::swap对两个数组进行逐元素交换,但你不能使用std::exchange对数组进行逐元素交换(因为数组不能从函数中返回)。
11. 数组可能由于别名引起性能下降
很容易无意中编写出由别名引起的性能损失的代码。考虑下面这些函数,它们用于将32位无符号整数数组以小端字节顺序序列化为字节数组。
std::array<std::byte, 4> serialize_le(unsigned x) {
    return {
        std::byte(x >>  0),
        std::byte(x >>  8),
        std::byte(x >> 16),
        std::byte(x >> 24)
    };
}

void write_le_n(std::byte* mem, std::array<unsigned, 1024>& numbers) {
    for (unsigned n : numbers) {
        auto bytes = serialize_le(n);
        std::memcpy(mem, &bytes[0], sizeof(bytes));
        mem += 4;
    }
}

void write_le(std::byte mem[], unsigned x) {
    mem[0] = std::byte(x >>  0);
    mem[1] = std::byte(x >>  8);
    mem[2] = std::byte(x >> 16);
    mem[3] = std::byte(x >> 24);
}

void write_le_n(std::byte* mem, unsigned x[1024]) {
    for (unsigned i = 0; i < 1024; ++i) {
        write_le(mem + i * 4, x[i]);
    }
}

请参见编译器资源管理器上的实时示例

直观地看,这两个代码示例似乎做的事情基本相同。然而,第二个示例的代码生成要糟糕得多。 完全没有自动向量化;这是一个极其简单的循环。

std::array通常可以告诉编译器没有别名发生,或者如果内存区域之间存在重叠,它不能是部分的。这对优化是一种提升。

结论

std::array解决了使用C风格数组时的无数问题。 在大多数情况下,这与性能无关,尽管在特定情况下std::array可能更好。 这是因为C风格数组具有令人困惑的语法、常见陷阱、任意限制和其他问题。

另请参阅


1

虽然std::array有一些优点,如其他有用的答案所述,但有些人声称,“……与原始数组相比没有性能差异。”

这简直是不真实的,对于任何从事真正嵌入式开发(时间关键、裸机)的人来说都是误导性的。

在一个运行在50MHz(CPU)的STM32G0B1上进行快速而简单的测试:向std::array写入大约需要比向原始C样式数组多5微秒。对我来说,这是一个显著的性能差异,不能被忽视。


介意发布你的基准测试吗? - Matt Spicer
@MattSpicer 这只是一个快速测试。很容易做到,不值得保留。简单地设置/清除测试引脚以给示波器提供脉冲,并测量写入一种类型的数组与另一种类型的数组所需的时间。 - Todd

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