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

And*_*nck 311 c++ arrays standards variable-length variable-length-array

在过去的几年里,我并没有非常使用过C语言.当我今天读到这个问题时,我遇到了一些我不熟悉的C语法.

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

void foo(int n) {
    int values[n]; //Declare a variable length array
}
Run Code Online (Sandbox Code Playgroud)

这似乎是一个非常有用的功能.有没有关于将它添加到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
}
Run Code Online (Sandbox Code Playgroud)

vector版本变得很笨拙:

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

切片,行和列也可能遍布整个内存.

看一下comp.std.c++这个问题的讨论很明显,这个问题在争论的两个方面都有一些非常重要的名字引起争议.毫无疑问,a std::vector总是更好的解决方案.

Quu*_*one 201

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

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

  • sizeof x不再总是编译时常量; 编译器有时必须生成代码以sizeof在运行时评估-expression.

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

  • 在C++世界中不那么重要,但对于C的嵌入式系统程序员的目标受众来说非常重要,声明VLA意味着扼杀任意大量的堆栈.这是一个保证堆栈溢出和崩溃.(无论何时你声明int A[n],你都隐含断言你有2GB的堆栈需要备用.毕竟,如果你知道" n这里肯定小于1000",那么你只需要声明int A[1000].用32位整数n代替1000是一个承认你不知道你的程序的行为应该是什么.)

好的,让我们现在开始讨论C++.在C++中,我们在C89所做的"类型系统"和"价值系统"之间有着相同的强烈区别......但我们真的开始以C没有的方式依赖它.例如:

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

如果n不是编译时常量(即,如果A是可变修改类型),那么究竟是什么类型S?会S的类型只能在运行时确定?

那这个呢:

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

编译器必须为某些实例化生成代码myfunc.该代码应该是什么样的?如果我们不知道A1编译时的类型,我们如何静态生成该代码?

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

基本上,C++正朝着将越来越多的决策推向编译时的方向发展:模板代码生成,constexpr功能评估等.同时,C99正忙于将传统的编译时决策(例如sizeof)推送到运行时.考虑到这一点,尝试将C99风格的VLA集成到C++ 中是否真的有意义?

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


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

  • 我同意VLA是错的.更广泛实现,更有用的`alloca()`应该在C99中标准化.当标准委员会在实施之前跳出而不是相反时,会发生VLA. (15认同)
  • 至于嵌入式,我几乎只使用嵌入式系统,并且一直使用指向 VLA 的指针。然而,我的编码标准禁止分配 VLA 对象。但我不记得在任何嵌入式系统中见过由 VLA 引起的堆栈溢出。“反VLA运动”似乎来自PC界,以微软为首。因为如果允许 VLA,MS 就必须从 1989 年更新其所谓的“2019”编译器,以与该语言的 1999 版本保持一致。 (14认同)
  • 可变修改型系统是IMO的一个很好的补充,你的子弹点都没有违反常识.(1)C标准不区分"编译时"和"运行时",因此这不是问题; (2)`*`是可选的,你可以(并且应该)写`int A [] [n]`; (3)您可以使用类型系统而无需实际声明任何VLA.例如,函数可以接受可变修改类型的数组,并且可以使用不同维度的非VLA 2-D数组调用它.但是,您在帖子的后半部分中有效积分. (8认同)
  • 好吧,C++ 编译器通过允许 VLA 作为扩展以某种方式解决了这些问题。您还错过了关于小尺寸 VLA 和大小等于最大 VLA 大小的固定数组之间的差异的一点 - 您必须默认构造静态数组的所有元素,但您可能无法做到! (4认同)
  • '*毕竟,如果您知道“n 在这里肯定小于 1000”,那么您只需声明 int A[1000].*' 就是无稽之谈。例如,如果 VLA 长度在 99.99% 的函数调用中为“10”,而在 0.01% 的调用中仅达到其上限“1000”,则您基本上浪费了 1000 个字节,只要frame 保留在堆栈中——如果函数在您的控制流层次结构中处于高位,则几乎所有时间都在堆栈中。您可能认为 1000 字节并不多,但是每次 CPU 必须移入和移出该函数时,都要考虑所有缓存未命中! (4认同)
  • @Jeff:你的测试用例中`n`的最大值是多少,你的堆栈大小是多少?我建议你尝试输入一个至少与堆栈大小一样大的`n`值.(如果用户无法在程序中控制`n`的值,那么我建议你只将`n`的最大值直接传播到声明中:声明`int A [1000]`或者不管它你是否需要.当"n"的最大值不受任何小的编译时常数限制时,VLA只是必需的,而且只是危险的.) (3认同)
  • 您知道,即使支持 VLA,您描述的所有有用的东西也将继续存在 - 它只是不会针对 VLA 进行编译,而只会针对固定长度的数组进行编译。并不是说我不同意你对 VLA 的保留,只是他们更多的是_使用_而不是拥有该功能。对函数指针执行“void *”的“reinterpret_cast”也意味着意外行为和可能的堆栈溢出,但这并不意味着它不应该是可能的。 (2认同)
  • “凭经验声明,VLA意味着切断堆栈中的任意大块。这是有保证的堆栈溢出和崩溃。(每次声明int A [n]时,您都在隐式断言您有2GB的堆栈可用空间”) false。我只是运行了VLA程序,其堆栈远小于2GB,而没有任何堆栈溢出。 (2认同)
  • 由于alloca()可以使用此类内在函数实现,因此按定义,可以将alloca()作为编译器标准函数在任何平台上实现都是正确的。没有理由,编译器无法检测到alloca()的第一个实例,并且无法将标记和发布的类型安排在代码中嵌入,并且没有理由,编译器无法使用堆来实现alloca()。它不能用堆栈来完成。硬/不可移植的是在C编译器的顶部实现alloca(),以便它可以在各种编译器和操作系统上工作。 (2认同)
  • @SergeyA:关于“你可能无法[默认构造`A`]”的有趣评论。正确:在“A vla[n]”的长度动态确定为 0 的一种特殊情况下,我们根本不需要调用“A::A()”。那么,编译器是否应该要求 `A::A()` 存在并且可调用?Clang 的 VLA 扩展说是;GCC 的扩展名是“有时”。https://godbolt.org/z/DDW4Lx(并重新“解决了这些问题”,请参阅 https://godbolt.org/z/Guvxoo 。) (2认同)

Joh*_*itb 192

最近在usenet上开始讨论这个问题:为什么C++ 0x中没有VLA.

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

C99 VLA可以提供一个很小的好处,即能够创建小型数组而不浪费空间或为未使用的元素调用构造函数,但它们会对类型系统引入相当大的更改(您需要能够根据运行时值指定类型 - 这当前的C++中尚不存在,除了new运算符类型说明符,但它们是专门处理的,因此运行时不会逃避new运算符的范围).

您可以使用std::vector,但它不完全相同,因为它使用动态内存,并使其使用自己的堆栈分配器并不是非常容易(对齐也是一个问题).它也没有解决同样的问题,因为向量是可调整大小的容器,而VLA是固定大小的.在C++动态阵列提议旨在介绍基于库的溶液,作为替代基于VLA的语言.但是,据我所知,它不会成为C++ 0x的一部分.

  • +1并接受.但有一条评论,我认为安全性论点有点弱,因为还有很多其他方法导致堆栈溢出.安全参数可用于支持您永远不应使用递归的位置,并且应该从堆中分配*all*对象. (21认同)
  • 所以你说这是因为还有其他方法可以导致堆栈溢出,我们不妨鼓励更多的堆栈溢出? (17认同)
  • 另请参阅Matt Austern在该主题中的答案:由于C++中更严格的类型匹配,因此V语言的语言规范可能要复杂得多(例如:C允许将`T(*)[]`分配给`T (*)[N]` - 在C++中这是不允许的,因为C++不知道"类型兼容性" - 它需要完全匹配),类型参数,异常,con和析构函数和东西.我不确定VLA的好处是否会真正为所有工作带来回报.但是,我从未在现实生活中使用过VLA,所以我可能不知道它们的好用例. (10认同)
  • @Andreas,同意了这个弱点.但是对于递归,在堆栈被吃掉之前需要大量的调用,如果可能的话,人们会使用迭代.正如usenet线程中的一些人所说的那样,在所有情况下,这并不是反对VLA的论据,因为有时您肯定知道上限.但是在那些情况下,从我看到的静态数组同样就足够了,因为它不会浪费太多空间(如果它*会*,那么你实际上必须再次询问堆栈区域是否足够大). (2认同)

Pfh*_*yer 24

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

void foo (int n)
{
    int *values = (int *)alloca(sizeof(int) * n);
}
Run Code Online (Sandbox Code Playgroud)

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

快速说明:正如在用于alloca(3)的Mac OS X手册页中所提到的,"alloca()函数依赖于机器和编译器;它的使用是不受欢迎的." 你知道吗

  • 此外,alloca()的范围是整个函数,而不仅仅是包含变量的代码块.因此在循环内使用它会不断增加堆栈.VLA没有这个问题. (3认同)
  • 但是,具有封闭块范围的VLA意味着它们比具有整个功能范围的alloca()明显更不实用.考虑:`if(!p){p = alloca(strlen(foo)+1); strcpy(p,foo); 这不能用VVA完成,正是因为它们的块范围. (3认同)
  • 这不能回答 OP 的 _why_ 问题。此外,这是一个类似`C` 的解决方案,而不是真正的`C++`-ish。 (2认同)

小智 13

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

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

  • 这样做的缺点是必须手动管理堆栈,但这通常是一个非常好的方法。 (2认同)

Vik*_*ehr 12

似乎它将在C++ 14中提供:

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

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

  • 维基百科不是规范参考:)这个提议没有进入C++ 14. (10认同)
  • @ViktorSehr:此wrt C ++ 17的状态如何? (2认同)
  • @einpoklum不知道,使用 boost::container::static_vector (2认同)

Ben*_*son 11

在某些情况下,与执行的操作相比,分配堆内存非常昂贵.一个例子是矩阵数学.如果你使用小型矩阵说5到10个元素并做很多算术,那么malloc开销将非常重要.同时使大小成为编译时间常数看起来非常浪费且不灵活.

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


phi*_*red 7

这被认为包含在C++/1x中,但被删除(这是对我之前所说的更正).

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

  • 不,我们没有,std :: vector不会在堆栈上分配数据.:) (41认同)
  • "堆栈"是一个实现细节; 只要满足对象生命周期的保证,编译器就可以从任何地方分配内存. (7认同)
  • @MM 实施质量不可移植。如果你不需要性能,你首先就不会使用 C++ (3认同)
  • @MM:很公平,但在实践中我们仍然不能使用 `std::vector` 代替,比如说,`alloca()`。 (2认同)