为什么可变长度数组不是 C++ 标准的一部分?

Why aren't variable-length arrays part of the C++ standard?

提问人:Andreas Brinck 提问时间:12/11/2009 最后编辑:Jan SchultkeAndreas Brinck 更新时间:9/26/2023 访问量:205029

问:

在过去的几年里,我很少使用C语言。当我今天读到这个问题时,我遇到了一些我不熟悉的 C 语法。

显然,在 C99 中,以下语法是有效的:

void foo(int n) {
    int values[n]; //Declare a variable length array
}

这似乎是一个非常有用的功能。有没有讨论过将其添加到 C++ 标准中,如果有,为什么省略了它?

一些可能的原因:

  • 毛茸茸的编译器供应商实现
  • 与标准的其他部分不兼容
  • 可以使用其他 C++ 构造模拟功能

C++ 标准规定数组大小必须是常量表达式 (8.3.4.1)。

是的,我当然意识到在玩具示例中可以使用 ,但这是从堆而不是堆栈中分配内存。如果我想要一个多维数组,比如:std::vector<int> values(m);

void foo(int x, int y, int z) {
    int values[x][y][z]; // Declare a variable length array
}

这个版本变得非常笨拙:vector

void foo(int x, int y, int z) {
    vector< vector< vector<int> > > values( /* Really painful expression here. */);
}

切片、行和列也可能分布在整个内存中。

从讨论中可以看出,这个问题非常有争议,争论双方都有一些非常重量级的名字。当然,这并不是一个更好的解决方案。comp.std.c++std::vector

C++ 数组 语言设计 数组 可变长度

评论

6赞 Dimitri C. 12/11/2009
只是出于好奇,为什么需要在堆栈上分配它?您是否害怕堆分配性能问题?
56赞 Andreas Brinck 12/11/2009
@Dimitri 不是真的,但不可否认的是,堆栈分配将比堆分配更快。在某些情况下,这可能很重要。
20赞 Calmarius 10/25/2010
可变长度数组的主要优点是所有数据都靠近在一起,因此当您遍历此数组时,您可以并排读取和写入字节。您的数据被提取到缓存中,CPU 可以处理它,而无需提取字节并将其发送到内存或从内存发送字节。
5赞 Yury 12/21/2012
可变长度数组也可用于将预处理器常量替换为静态常量变量。同样在 C 中,您没有其他 VLA 选项,有时需要编写可移植的 C/C++ 代码(与两个编译器兼容)。
5赞 user3426763 3/17/2014
顺便说一句,似乎 clang++ 允许 VLA。

答:

1赞 Dimitri C. 12/11/2009 #1

为此,请使用 std::vector。例如:

std::vector<int> values;
values.resize(n);

内存将在堆上分配,但这只有一个很小的性能缺点。此外,明智的做法是不要在堆栈上分配大型数据块,因为它的大小相当有限。

评论

9赞 AHelps 4/11/2015
可变长度数组的一个主要应用是任意阶多项式的计算。在这种情况下,您的“小性能缺陷”意味着“在典型情况下,代码的运行速度慢了五倍”。这可不小。
3赞 L. F. 10/6/2019
你为什么不简单地使用?通过在施工后使用,您禁止不可移动的类型。std::vector<int> values(n);resize
0赞 CodingLab 12/27/2021
不等同。脏语法。
0赞 v010dya 7/19/2023
@AHelps 根据我的经验,99% 想要 VLA 的人这样做并不是因为他们想要与任意阶多项式有任何关系,而是因为他们想要一个存储,但在用户输入某些内容之前不知道要提供什么值。在这种情况下,这个解决方案是最好的。
0赞 AHelps 9/29/2023
@v010dya 是的,但这意味着使用我的用例(包括物理引擎、字体渲染、CAD、计算机图形学和数学库)的人无法编写最高效的代码。也许这些类型的应用程序真的只有 1% 的人。也许我们应该找到一种比 C++ 更好的语言吗?
1赞 anon 12/11/2009 #2

像这样的数组是 C99 的一部分,但不是标准 C++ 的一部分。正如其他人所说,向量总是一个更好的解决方案,这可能就是为什么可变大小的数组不在 C++ 标准(或建议的 C++0x 标准中)的原因。

顺便说一句,对于“为什么”C++标准是这样的问题,主持的Usenet新闻组comp.std.c++是去的地方。

评论

14赞 Patrick M 7/11/2013
-1 Vector 并不总是更好。通常,是的。总是,没有。如果你只需要一个小数组,在一个堆空间很慢的平台上,并且你的库的 vector 实现使用堆空间,那么这个功能如果存在的话可能会更好。
0赞 Ben Voigt 11/7/2023
@PatrickM:FWIW,使用默认分配器时必须始终使用动态存储时长进行分配,因为使用自动存储时长无法满足移动和交换的后置条件。(请注意,当自定义分配器被激活时,当分配器不是同一个对象时,移动和交换被允许失败,所以这个特定问题就消失了)std::vector
12赞 philsquared 12/11/2009 #3

这被考虑包含在 C++/1x 中,但被删除了(这是对我之前所说的更正)。

无论如何,它在 C++ 中用处不大,因为我们已经必须填补这个角色。std::vector

评论

53赞 Kos 8/11/2011
不,我们没有,std::vector 不会在堆栈上分配数据。:)
3赞 einpoklum 5/11/2016
@M.M:很公平,但在实践中,我们仍然不能用 代替,比如说,.std::vectoralloca()
1赞 M.M 5/11/2016
@einpoklum 在为您的程序获得正确的输出方面,您可以。性能是一个实施质量问题
5赞 pal 8/9/2019
@M.M 的实施质量是不可移植的。如果你不需要性能,你一开始就不使用 C++
1赞 Abdurrahim 10/3/2021
如何在没有繁琐乘法的情况下使用向量处理多维。C++ 只给出排除有用工具的借口,而他们谎称“我们需要允许人们写我们无法想象的东西”。如果这是无用的,为什么所有这些语言都支持它:en.wikipedia.org/wiki/Variable-length_array 甚至 C# 都添加了它,是的,它被称为 stackalloc....
251赞 Johannes Schaub - litb 12/11/2009 #4

最近在usenet上有一个关于这个问题的讨论:为什么C++0x中没有VLA

我同意那些似乎同意在堆栈上创建一个潜在的大型数组的人,这通常只有很少的可用空间,这并不好。参数是,如果你事先知道大小,你可以使用静态数组。如果你事先不知道大小,你就会写出不安全的代码。

C99 VLA 可以提供一个小好处,即能够在不浪费空间或为未使用的元素调用构造函数的情况下创建小数组,但它们会给类型系统带来相当大的更改(您需要能够根据运行时值指定类型 - 这在当前的 C++ 中尚不存在,除了运算符类型说明符, 但是它们被特殊处理,因此运行时性不会超出运算符的范围。newnew

您可以使用 ,但它并不完全相同,因为它使用动态内存,并且让它使用自己的堆栈分配器并不容易(对齐也是一个问题)。它也不能解决同样的问题,因为向量是一个可调整大小的容器,而 VLA 是固定大小的。C++ 动态数组提案旨在引入基于库的解决方案,作为基于语言的 VLA 的替代方案。但是,据我所知,它不会成为 C++0x 的一部分。std::vector

评论

35赞 Andreas Brinck 12/11/2009
+1 并被接受。不过有一点评论,我认为安全论点有点弱,因为还有很多其他方法可以导致堆栈溢出。安全参数可用于支持不应使用递归,并且应从堆中分配所有对象的立场。
26赞 jalf 12/11/2009
所以你是说,因为还有其他方法可以导致堆栈溢出,我们不妨鼓励更多?
3赞 Johannes Schaub - litb 12/11/2009
@Andreas,同意弱点。但是对于递归来说,它需要大量的调用,直到堆栈被吃掉,如果是这样的话,人们会使用迭代。但是,正如Usenet线程上的一些人所说,这并不是在所有情况下都反对VLA的论点,因为有时您肯定知道上限。但是在这些情况下,据我所知,静态数组同样足够了,因为它无论如何都不会浪费太多空间(如果,那么您实际上必须再次询问堆栈区域是否足够大)。
11赞 Johannes Schaub - litb 12/11/2009
另请查看Matt Austern在该线程中的回答: VLA 的语言规范对于 C++ 来说可能会复杂得多,因为 C++ 中的类型匹配更严格(例如:C 允许将 a 分配给 a - 在 C++ 中这是不允许的,因为 C++ 不知道“类型兼容性” - 它需要精确匹配)、类型参数、 异常、析构函数和析构函数之类的东西。我不确定 VLA 的好处是否真的能带来所有这些工作的回报。但是,我从未在现实生活中使用过 VLA,所以我可能不知道它们的好用例。T(*)[]T(*)[N]
1赞 supercat 6/4/2015
@AHelps:也许最适合的是一种类型,它的行为有点像,但需要固定的后进先出使用模式,并维护一个或多个每个线程静态分配的缓冲区,这些缓冲区的大小通常根据线程有史以来使用过的最大总分配来调整,但可以显式修剪。在一般情况下,正常的“分配”只需要指针复制、指针从指针减法、整数比较和指针加法;取消分配只需要一个指针副本。不比 VLA 慢多少。vector
37赞 PfhorSlayer 12/11/2009 #5

如果您愿意,您可以随时使用 alloca() 在运行时在堆栈上分配内存:

void foo (int n)
{
    int *values = (int *)alloca(sizeof(int) * n);
}

在堆栈上分配意味着当堆栈展开时,它将自动释放。

快速说明:正如 Mac OS X 手册页中提到的 alloca(3) ,“alloca() 函数依赖于机器和编译器;不鼓励使用它。只是为了让你知道。

评论

9赞 sashoalm 7/1/2016
此外,alloca() 的作用域是整个函数,而不仅仅是包含变量的代码块。因此,在循环中使用它将不断增加堆栈。VLA 没有此问题。
4赞 MadScientist 1/13/2017
但是,具有封闭块范围的 VLA 意味着它们在整个函数的作用域上明显不如 alloca() 有用。请考虑:VLA 无法做到这一点,正是因为它们的块范围。if (!p) { p = alloca(strlen(foo)+1); strcpy(p, foo); }
3赞 Adrian W 6/25/2018
这并不能回答OP的“为什么”问题。此外,这是一个类似解决方案,而不是真正的解决方案。CC++
0赞 CodingLab 12/27/2021
不等同。Alloca 的语法很脏。
16赞 Bengt Gustafsson 1/23/2011 #6

在某些情况下,与执行的操作相比,分配堆内存的成本非常高。一个例子是矩阵数学。如果你使用小矩阵,比如说 5 到 10 个元素,并做大量的算术运算,那么 malloc 开销将非常大。同时,将大小作为编译时常量似乎非常浪费且不灵活。

我认为 C++ 本身非常不安全,以至于“尽量不添加更多不安全功能”的论点不是很强。另一方面,由于 C++ 可以说是运行时效率最高的编程语言,因此它总是有用的:编写性能关键型程序的人将在很大程度上使用 C++,并且他们需要尽可能多的性能。将东西从堆移动到堆栈就是这样一种可能性。减少堆块的数量是另一回事。允许 VLA 作为对象成员是实现此目的的一种方法。我正在研究这样的建议。诚然,实现起来有点复杂,但似乎完全可行。

27赞 Eric 3/22/2013 #7

在我自己的工作中,我意识到,每次我想要像可变长度自动数组或 alloca() 这样的东西时,我并不真正关心内存是否物理位于 cpu 堆栈上,只是它来自某个堆栈分配器,它不会导致缓慢的到通用堆。因此,我有一个每个线程对象,它拥有一些内存,它可以从中推送/弹出可变大小的缓冲区。在某些平台上,我允许它通过 mmu 增长。其他平台具有固定大小(通常也伴随着固定大小的 cpu 堆栈,因为没有 mmu)。我使用的一个平台(掌上游戏机)无论如何都有宝贵的小 cpu 堆栈,因为它驻留在稀缺的快速内存中。

我并不是说永远不需要将可变大小的缓冲区推送到 cpu 堆栈上。老实说,当我发现这不是标准时,我感到很惊讶,因为这个概念似乎很适合语言。不过,对我来说,“可变大小”和“必须物理位于 cpu 堆栈上”的要求从未一起出现过。这与速度有关,所以我制作了自己的“数据缓冲区并行堆栈”。

评论

2赞 Smiley1000 8/6/2021
这样做的缺点是必须手动管理该堆栈,但这通常是一种非常好的方法。
0赞 Ben 2/3/2022
我也想过这个问题......你是说,当你想要线程本地暂存空间时,你实际上有一个可以从任何地方获得的?thread_local std::pmr::unsynchronized_pool_resource;
17赞 Viktor Sehr 8/13/2013 #8

似乎它将在 C++14 中可用:

https://en.wikipedia.org/wiki/C%2B%2B14#Runtime-sized_one_dimensional_arrays

更新:它没有进入 C++14。

评论

0赞 default 8/13/2013
有趣。Herb Sutter 在 Dynamic Arraysisocpp.org/blog/2013/04/trip-report-iso-c-spring-2013-meeting 下对此进行了讨论(这是维基百科信息的参考)
1赞 strager 2/24/2014
2014 年 1 月 18 日在维基百科上写道:“运行时大小的数组和 dynarray 已移至数组扩展技术规范”:en.wikipedia.org/w/...
15赞 M.M 11/4/2014
维基百科不是一个规范性的参考:)这个提议没有进入C++14。
2赞 einpoklum 5/11/2016
@ViktorSehr:这个w.r.t.C++17的状态如何?
2赞 Viktor Sehr 3/2/2017
@einpoklum 不知道,请使用 boost::container::static_vector
364赞 Quuxplusone 2/3/2014 #9

(背景:我有一些实现 C 和 C++ 编译器的经验。

C99 中的可变长度数组基本上是一个失误。为了支持VLA,C99必须对常识做出以下让步:

  • sizeof x不再始终是编译时常量;编译器有时必须生成代码才能在运行时计算 -expression。sizeof

  • 允许二维 VLA () 需要一种新的语法来声明将 2D VLA 作为参数的函数:。int A[x][y]void foo(int n, int A[][*])

  • 在 C++ 世界中不太重要,但对于 C 的目标受众嵌入式系统程序员来说极为重要,声明 VLA 意味着占用任意块堆栈。这是有保证的堆栈溢出和崩溃。(每当您声明 时,您都隐含地断言您有 2GB 的堆栈可用。毕竟,如果你知道“这里肯定少于1000”,那么你只会声明.将 32 位整数替换为 是承认您不知道程序的行为应该是什么。int A[n]nint A[1000]n1000

好的,现在让我们开始讨论 C++。在 C++ 中,我们在“类型系统”和“值系统”之间有同样强烈的区别,就像 C89 一样......但我们真的开始以 C 语言没有的方式依赖它。例如:

template<typename T> struct S { ... };
int A[n];
S<decltype(A)> s;  // equivalently, S<int[n]> s;

如果不是编译时常量(即,如果是可变修改的类型),那么到底是什么类型?的类型是否也仅在运行时确定?nASS

这个呢:

template<typename T> bool myfunc(T& t1, T& t2) { ... };
int A1[n1], A2[n2];
myfunc(A1, A2);

编译器必须生成代码以对 进行某些实例化。该代码应该是什么样子的?如果我们在编译时不知道该代码的类型,我们如何静态生成该代码?myfuncA1

更糟糕的是,如果在运行时发现,那么呢?在这种情况下,调用 甚至不应该编译,因为模板类型推导应该失败!我们怎么可能在运行时模拟这种行为?n1 != n2!std::is_same<decltype(A1), decltype(A2)>()myfunc

基本上,C++ 正朝着将越来越多的决策推向编译时的方向发展:模板代码生成、函数评估等等。与此同时,C99 正忙于将传统的编译时决策(例如 )推入运行时。考虑到这一点,花费任何精力尝试将 C99 样式的 VLA 集成到 C++ 中真的有意义吗?constexprsizeof

正如其他所有回答者已经指出的那样,当你真的想传达“我不知道我可能需要多少RAM”的想法时,C++ 提供了许多堆分配机制(或者是显而易见的机制)。C++ 提供了一个漂亮的异常处理模型,用于处理不可避免的情况,即您需要的 RAM 量大于您拥有的 RAM 量。但希望这个答案能让你很好地了解为什么 C99 风格的 VLA 不适合 C++——甚至不适合 C99。;)std::unique_ptr<int[]> A = new int[n];std::vector<int> A(n);


有关该主题的更多信息,请参阅 N3810“阵列扩展的替代方案”,Bjarne Stroustrup 2013 年 10 月关于 VLA 的论文。Bjarne 的 POV 与我的 POV 非常不同;N3810 更侧重于为事物找到一个好的 C++ish 语法,并阻止在 C++ 中使用原始数组,而我更关注对元编程和类型系统的影响。我不知道他是否认为元编程/类型系统的影响已经解决、可以解决,或者只是无趣。


一篇很好的博客文章是“可变长度数组的合法使用”(Chris Wellons,2019-10-27)。

评论

30赞 MadScientist 3/25/2014
我同意 VLA 是错误的。更广泛地实现,更有用的,应该在C99中标准化。VLA 是当标准委员会在实施之前跳出来时发生的情况,而不是相反。alloca()
16赞 M.M 3/17/2015
可变修改的类型系统是一个很好的补充 IMO,您的任何要点都不会违反常识。(1)C标准没有区分“编译时”和“运行时”,所以这不是问题;(2)是可选的,你可以(也应该)写;(3) 您可以在不实际声明任何 VLA 的情况下使用类型系统。例如,函数可以接受可变修改类型的数组,并且可以使用不同维度的非 VLA 二维数组调用它。但是,您在帖子的后半部分提出了有效的观点。*int A[][n]
8赞 Jeff Hammond 6/1/2016
“声明 VLA 意味着占用堆栈的任意大块。这是有保证的堆栈溢出和崩溃。(每当你声明 int A[n] 时,你都在隐含地断言你有 2GB 的堆栈可用“在经验上是错误的。我刚刚运行了一个堆栈远小于 2GB 的 VLA 程序,没有任何堆栈溢出。
31赞 Will 4/19/2019
'毕竟,如果你知道“这里 n 肯定小于 1000”,那么你只需声明 int A[1000]。简直是胡说八道。例如,如果 VLA 长度在 99.99% 的函数调用上,并且只在 0.01% 的调用上达到其上限,那么您基本上浪费了 1000 个字节,只要帧保留在堆栈上,这些字节就永远不会被释放——如果函数位于控制流层次结构的较高位置,则几乎一直如此。您可能认为 1000 字节并不多,但每次 CPU 必须移入和移出该函数时,都要考虑所有缓存丢失!101000
13赞 Lundin 3/19/2021
至于嵌入式,我几乎只使用嵌入式系统,并且一直使用指向 VLA 的指针。但是,我的编码标准禁止分配 VLA 对象。但我不记得在任何嵌入式系统中看到过由 VLA 引起的堆栈溢出。“反VLA运动”似乎来自PC人,Microsoft在前面。因为如果允许 VLA,MS 将不得不从 1989 年开始更新他们所谓的“2019”编译器,以与该语言的 1999 版本保持一致。
3赞 tstanisl 9/17/2021 #10

VLA 是更大的可变修改类型系列的一部分。 这一系列类型非常特殊,因为它们具有运行时组件。

代码:

int A[n];

被编译器视为:

typedef int T[n];
T A;

请注意,数组的运行时大小不绑定到变量,而是绑定到变量的类型A

没有什么能阻止人们创建这种类型的新变量:

T B,C,D;

或指针或数组

T *p, Z[10];

此外,指针允许创建具有动态存储的 VLA。

T *p = malloc(sizeof(T));
...
free(p);

是什么消除了一个流行的神话,即 VLA 只能在堆栈上分配。

回到问题。

这个运行时组件不能很好地与类型推导配合,类型推导是 C++ 类型系统的基础之一。不可能使用模板、扣除和重载。

C++类型系统是静态的,所有类型都必须在编译过程中完全定义或推导。 VM 类型仅在程序执行期间完成。 将 VM 类型引入已经非常复杂的 C++ 的额外复杂性被认为是不合理的。主要是因为它们的主要实际应用 是自动 VLA (),其替代形式为 。int A[n];std::vector

这有点令人难过,因为 VM 类型为处理多维数组的程序提供了非常优雅和高效的解决方案。

在 C 语言中,可以简单地写成:

void foo(int n, int A[n][n][n]) {
  for (int i = 0; i < n; ++i)
    for (int j = 0; j < n; ++j)
      for (int k = 0; k < n; ++k)
        A[i][j][k] = i * j * k;
}

...

int A[5][5][5], B[10][10][10];
foo(5, A);
foo(10, B);

现在尝试在 C++ 中提供高效和优雅的解决方案。