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


326

在过去的几年中,我很少使用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总是可以找到更好的解决方案。


3
出于好奇,为什么需要在堆栈上分配它?您是否很担心堆分配性能问题?
Dimitri C.

32
@Dimitri并非如此,但是不可否认的是堆栈分配比堆分配要快。在某些情况下,这可能很重要。
安德烈亚斯·布林克

11
可变长度数组的主要优点是所有数据都紧密靠近,因此当您遍历该数组时,您会彼此相邻地读取和写入字节。您的数据被提取到缓存中,而cpu可以在其上工作,而无需提取字节并将其发送到内存或从内存发送字节。
Calmarius

4
可变长度数组也可用于用静态const变量替换预处理器常量。同样在C语言中,VLA没有其他选项,有时需要编写可移植的C / C ++代码(与两个编译器兼容)。
尤里

2
顺便说一句,似乎clang ++允许VLA。
user3426763 2014年

Answers:


204

最近在usenet中开始了对此的讨论:为什么C ++ 0x中没有VLA

我同意那些似乎同意必须在堆栈上创建潜在的大型阵列(通常只有很少的可用空间)的做法不好的人。该参数是,如果您事先知道大小,则可以使用静态数组。而且,如果您事先不知道大小,您将编写不安全的代码。

C99 VLA可以提供一个小的好处,即能够创建小型数组而不会浪费空间或为未使用的元素调用构造函数,但是它们会给类型系统带来相当大的变化(您需要能够根据运行时值指定类型-这除new运算符类型说明符外,当前C ++中尚不存在该表达式,但对它们进行了特殊处理,因此运行时不会逃脱new运算符的范围)。

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


22
+1并被接受。不过,有一种评论认为,由于存在许多其他导致堆栈溢出的方法,因此安全性论点有点弱。安全性参数可用于支持您永远不要使用递归和应该分配堆中所有对象的立场。
Andreas Brinck

17
因此,您说的是,因为还有其他导致堆栈溢出的方法,我们还是鼓励更多的方法吗?
jalf

3
@Andreas,对弱点表示同意。但是对于递归而言,要消耗大量的调用才能耗尽堆栈,如果是这种情况,人们将使用迭代。正如Usenet线程上的一些人所说,这并不是在所有情况下都反对VLA,因为有时您肯定会知道上限。但是在那些情况下,从我的角度来看,静态数组就足够了,因为无论如何它都不会浪费太多的空间(如果这样的,那么您实际上必须再次询问堆栈区域是否足够大)。
Johannes Schaub-litb

10
还要看一下该线程中的Matt Austern的回答:由于C ++中类型匹配更严格,因此VLA的语言规范对于C ++可能要复杂得多(例如:C允许在C ++中将- 分配T(*)[]T(*)[N]--这是不允许的,因为C ++不了解“类型兼容性”,它需要完全匹配),类型参数,异常,构造函数和析构函数以及其他内容。我不确定VLA的好处是否真的能付清所有工作。但是后来,我在现实生活中从未使用过VLA,因此我可能不知道它们的良好用例。
Johannes Schaub-litb

1
@AHelps:也许对此最好的类型是行为类似于vector但需要固定的LIFO使用模式并维护一个或多个每个线程静态分配的缓冲区的类型,该缓冲区通常根据线程具有的最大总分配来确定大小曾经使用过,但是可以明确地进行修剪。通常情况下,正常的“分配”只需要指针复制,指针与指针相减,整数比较和指针加法即可。取消分配仅需要指针副本。不比VLA慢很多。
supercat 2015年

216

(背景:我有一些实现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位整数替换n1000允许值)您不知道程序的行为应该是什么。)

好的,现在让我们开始讨论C ++。在C ++中,我们在“类型系统”和“值系统”之间的区别与C89一样强,但是我们实际上已经开始以C所没有的方式来依赖它。例如:

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

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

那这个呢:

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

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

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

基本上,C ++朝着将越来越多的决策推向编译时的方向发展:模板代码生成,constexpr函数评估等。同时,C99忙于将传统的编译时决策(例如sizeof)推送到运行时。考虑到这一点,它真的甚至是有意义的花费任何力气试图以C99风格VLAS整合到C ++?

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


有关该主题的更多信息,请参见Bjarne Stroustrup在2013年10月发布的有关VLA的N3810“数组扩展的替代方案”。Bjarne的POV与我的有很大不同。N3810更着重于为事物找到良好的C ++ ish 语法,并阻止在C ++中使用原始数组,而我更着重于元编程和类型系统的含义。我不知道他是否认为元编程/类型系统的含义已解决,可解决或仅无趣。


一个很好的博客文章也提到了很多相同的问题,例如“合法使用可变长度数组”(Chris Wellons,2019-10-27)。


15
我同意VLA只是错误的。alloca()相反,应该在C99中将实现范围更广,用途更广的标准进行标准化。VLA是标准委员会在实施之前跳出来而不是相反的情况。
MadScientist 2014年

10
可变修改的类型系统是IMO的一个很好的补充,您的要点都不违反常识。(1)C标准没有区分“编译时”和“运行时”,因此这不是问题;(2)*是可选的,您可以(并且应该)编写int A[][n];(3)您可以使用类型系统,而无需实际声明任何VLA。例如,一个函数可以接受可变修改类型的数组,并且可以使用不同尺寸的非VLA 2-D数组调用该函数。但是,您在文章的后半部分提出了正确的观点。
MM

3
“凭经验声明VLA意味着切掉堆栈中的任意大块。这是保证的堆栈溢出和崩溃。(每次声明int A [n]时,您都在暗指断言您有2GB的堆栈可用空间”)我刚刚运行了一个VLA程序,其堆栈小于2GB,没有任何堆栈溢出
Jeff

3
@Jeff:n您的测试用例的最大值是多少,堆栈的大小是多少?我建议您尝试输入一个n至少与堆栈大小一样大的值。(而且,如果用户无法控制程序中的值n,那么我建议您仅将直接的最大值传播n到声明中:declaration int A[1000]或所需的任何内容。VLA仅是必需的,而且是危险的, (当的最大值n不受任何小的编译时常数限制时。)
Quuxplusone

2
由于alloca()可以使用此类内在函数实现,因此按定义,可以将alloca()作为编译器标准函数在任何平台上实现都是正确的。没有理由,编译器无法检测到alloca()的第一个实例,并且无法将标记和发布的类型安排在代码中嵌入,并且没有理由,编译器无法使用堆来实现alloca()。它不能用堆栈来完成。硬/不可移植的是 C编译器的顶部实现alloca(),以便它可在各种编译器和操作系统上工作。
MadScientist

26

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

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

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

快速说明:如Mac OS X手册页中针对alloca(3)所述,“ alloca()函数取决于计算机和编译器;不建议使用它。” 请注意。


4
同样,alloca()的范围是整个函数,而不仅仅是包含变量的代码块。因此,在循环内使用它会不断增加堆栈。VLA没有此问题。
sashoalm '16

3
但是,具有封闭块范围的VLA意味着它们在整个函数范围内的作用远不如alloca()有用。考虑:if (!p) { p = alloca(strlen(foo)+1); strcpy(p, foo); } 正是由于VLA的块范围,这不能用VLA做到。
MadScientist

1
那没有回答OP的为什么问题。而且,这是一个类似C的解决方案,而不是真正的解决方案C++
Adrian W

13

在我自己的工作中,我已经意识到,每当我想要可变长的自动数组或alloca()之类的东西时,我都不在乎内存实际上位于cpu堆栈上,而是因为它来自一些不会导致缓慢访问常规堆的堆栈分配器。因此,我有一个每个线程对象,该对象拥有一些内存,可以从中推/弹出可变大小的缓冲区。在某些平台上,我允许它通过mmu扩展。其他平台具有固定大小(通常还附带固定大小的cpu堆栈,因为没有mmu)。我使用的一个平台(手持游戏机)无论如何都具有宝贵的cpu堆栈,因为它驻留在稀缺的快速内存中。

我并不是说永远不需要将可变大小的缓冲区推入cpu堆栈。老实说,当我发现这不是标准时,我感到很惊讶,因为它看起来确实很适合这种语言。但是对我而言,“可变大小”和“必须物理上放置在cpu堆栈上”的要求从来没有提出来。这与速度有关,所以我做了自己的“用于数据缓冲区的并行堆栈”。


12

在某些情况下,与执行的操作相比,分配堆内存非常昂贵。一个例子是矩阵数学。如果您使用较小的矩阵,则说5到10个元素并进行大量的算术运算,那么malloc开销将非常重要。同时,将大小设为编译时间常数似乎确实非常浪费和僵化。

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


12

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

https://zh.wikipedia.org/wiki/C%2B%2B14#Runtime-sized_one_Dimension_arrays

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


有趣。Herb Sutter在“ 动态数组”下进行了讨论:isocpp.org/blog/2013/04/trip-report-iso-c-spring-2013-meeting(这是Wikipedia信息的参考)
默认

1
“运行时大小的数组和的DynArray已移至阵列扩展技术规范”在维基百科上写道:78.86.152.103 2014年1月18日:en.wikipedia.org/w/...
strager

10
Wikipedia不是规范性参考书:)这项建议并未纳入C ++ 14。
MM

2
@ViktorSehr:此wrt C ++ 17的状态如何?
einpoklum's

@einpoklum不知道,使用boost :: container :: static_vector
Viktor Sehr

7

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

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


42
不,我们不这样做,std :: vector不会在堆栈上分配数据。:)
Kos

7
“堆栈”是一个实现细节;编译器可以在任何地方分配内存,只要满足有关对象生存期的保证即可。
MM 2014年

1
@MM:足够公平,但实际上我们仍然不能std::vector代替alloca()
einpoklum's

@einpoklum可以为您的程序获取正确的输出。性能是实现质量的问题
MM

1
@MM的实现质量不可移植。如果您不需要性能,那么就不要使用c ++
pal,

3

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

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

内存将在堆上分配,但这只带来很小的性能缺陷。此外,明智的做法是不要在堆栈上分配大数据块,因为它的大小受到限制。


4
可变长度数组的主要应用是对任意多项式的求值。在这种情况下,您的“性能小缺点”意味着“在典型情况下,代码运行速度慢五倍”。那不小。
AHelps

1
您为什么不简单使用std::vector<int> values(n);?通过resize在构造之后使用,您禁止使用不可移动的类型。
LF

1

C99允许VLA。并且对如何声明VLA施加了一些限制。有关详细信息,请参阅标准的6.7.5.2。C ++禁止使用VLA。但是g ++允许它。


您能否提供指向您指向的标准段落的链接?
文森特

0

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

顺便说一句,对于“为什么”是C ++标准的问题,经过审核的Usenet新闻组comp.std.c ++是您可以去的地方。


6
-1 Vector并不总是更好。通常,是的。一直没有 如果您只需要一个小数组,并且在堆空间缓慢的平台上,并且您的库的vector实现使用堆空间,那么此功能(如果存在)可能会更好。
Patrick M

-1

如果您知道编译时的值,则可以执行以下操作:

template <int X>
void foo(void)
{
   int values[X];

}

编辑:由于分配器是模板参数,因此可以创建使用堆栈分配器(alloca)的向量。


18
如果您知道编译时的值,则根本不需要模板。只需在非模板函数中直接使用X。
罗伯·肯尼迪

3
有时,调用者在编译时知道而被调用者却不知道,这就是模板的优点。当然,在一般情况下,直到运行时才知道X。
Qwertie 2012年

您不能在STL分配器中使用alloca-从alloca中分配的内存将在堆栈帧被破坏时被释放-那时应该分配内存的方法返回。
奥利弗(Oliver)

-5

我有一个实际上对我有用的解决方案。我不想分配内存,因为需要多次运行的例程的碎片。答案非常危险,请您自担风险,但要利用组装的优势在堆栈上保留空间。我下面的示例使用一个字符数组(显然其他大小的变量将需要更多的内存)。

void varTest(int iSz)
{
    char *varArray;
    __asm {
        sub esp, iSz       // Create space on the stack for the variable array here
        mov varArray, esp  // save the end of it to our pointer
    }

    // Use the array called varArray here...  

    __asm {
        add esp, iSz       // Variable array is no longer accessible after this point
    } 
}

这里有很多危险,但我将解释一些:1.半途更改变量大小会杀死堆栈位置2.超出数组界限会破坏其他变量和可能的代码3.在64位中不起作用构建...需要不同的程序集(但是宏可以解决该问题)。4.特定于编译器(在编译器之间移动可能会遇到麻烦)。我没有尝试过,所以我真的不知道。


……如果您想自己动手,也许使用RAII类?
einpoklum

您可以简单地使用boost :: container :: static_vector。
维克多·瑟尔

对于没有比MSVC具有更多原始程序集的其他编译器,这没有等效项。VC可能会了解已esp更改的内容,并将调整其对堆栈的访问权限,但是在GCC中,您将完全破坏它-至少在使用优化的情况下-fomit-frame-pointer(尤其是使用优化)。
Ruslan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.