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

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

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

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

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

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

一些潜在原因:

  • 编译器供应商要实现的错误
  • 与本标准其他部分不兼容
  • 功能可以用其他C++构造来仿真。

C++标准声明数组大小必须是常数表达式(83.4.1)。

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

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

vector版本变得相当笨拙:

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

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

comp.std.c++的讨论来看,很明显,这个问题是有争议的,争论双方都有一些重量级人物。当然,并不明显,std::vector总是一个更好的解决方案。


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

C99中的可变长度阵列基本上是一个错误。为了支持VLA,C99必须根据常识做出以下让步:

  • sizeof x不再总是编译时常量;编译器有时必须生成代码来计算运行时的sizeof表达式。

  • 允许二维VLA(int A[x][y]需要一种新的语法来声明以二维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的方式。例如:

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

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

这个怎么样:

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

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

更糟的是,如果在运行时发现n1 != n2,那么!std::is_same()会怎么样?在这种情况下,对myfunc的调用甚至不应该编译,因为模板类型扣除应该失败!我们怎么可能在运行时模拟这种行为?

基本上,C++正朝着将越来越多的决策推到编译时间的方向发展:模板代码生成、EDCOX1、18函数评价等。与此同时,C99正忙于将传统的编译时决策(如sizeof)推送到运行时中。考虑到这一点,尝试将C99风格的VLAS集成到C++中是否真的有意义?

正如其他回答者已经指出的那样,C++提供了大量的堆分配机制(EDCOX1,20)或EDCOX1(21)是显而易见的,当你真的想传达"我不知道我需要多少RAM"的想法时,C++提供了一个极好的异常处理模型来处理不可避免的情况。您需要的RAM大于您拥有的RAM数量。但希望这个答案能让你很好地理解为什么C99风格的VLAS不适合C++,甚至不适合C99。;)

有关该主题的更多信息,请参阅n3810"阵列扩展的备选方案",bjarne stroustrup于2013年10月发表的关于VLA的论文。BJARNE的POV与我的不同,N38着重于寻找一个好的C++ ISH语法,并在C++中阻止使用原始数组,而我更多地关注元编程和类型系统的含义。我不知道他是否认为元编程/类型系统的含义已经解决、可以解决,或者仅仅是无趣。


最近有一个关于在USENET中启动的讨论:为什么C++0x中没有VLAs。

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

C99 VLAS可以提供一个小的好处,可以创建小数组而不浪费空间或调用未使用的元素的构造函数,但是它们会给类型系统带来相当大的变化(您需要根据运行时值来指定类型),除了EDCOX1·1操作符之外,在当前C++中还不存在这种类型。类型说明符,但它们是特殊处理的,这样运行时性就不会超出new运算符的范围。

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


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

1
2
3
4
void foo (int n)
{
    int *values = (int *)alloca(sizeof(int) * n);
}

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

快速说明:正如MacOSX的alloca(3)手册页中所提到的,"alloca()函数依赖于机器和编译器,它的使用被取消了。"正如你所知道的。


在我自己的工作中,我意识到每当我想要变长自动数组或alloca()之类的东西时,我并不真正关心内存在CPU堆栈上的物理位置,只是它来自一些堆栈分配器,而这些分配器不会导致到常规堆的缓慢访问。所以我有一个线程对象,它拥有一些内存,可以从中推/弹出可变大小的缓冲区。在某些平台上,我允许通过MMU实现增长。其他平台的大小是固定的(通常伴随着固定大小的CPU堆栈,因为没有MMU)。我使用的一个平台(一个手持游戏机)几乎没有CPU堆栈,因为它位于稀缺、快速的内存中。

我不是说永远不需要把可变大小的缓冲区推到CPU堆栈上。老实说,当我发现这不是标准的时候,我很惊讶,因为这个概念似乎很适合语言。不过,对我来说,"可变大小"和"必须物理地位于CPU堆栈上"的要求从来没有结合在一起。它是关于速度的,所以我自己做了一种"数据缓冲区的并行堆栈"。


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

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


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

https://en.wikipedia.org/wiki/c%2b%2b14运行时大小的单维数组

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


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

不管怎样,它在C++中都不那么有用,因为我们已经有了EDCOX1,0个来填充这个角色。


使用STD::向量。例如:

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

内存将在堆上分配,但这只会带来很小的性能缺陷。此外,最好不要在堆栈上分配大型数据块,因为它的大小非常有限。


C99允许VLA。它对如何声明VLA进行了一些限制。详见本标准6.7.5.2。C++不允许VLA。但G++允许。


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

顺便说一下,对于"为什么"C++标准的问题,适度的USENET新闻组COMP.ST.C++是要去的地方。


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

1
2
3
4
5
6
template <int X>
void foo(void)
{
   int values[X];

}

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。超过数组边界将破坏其他变量和可能的代码三。这在64位版本中不起作用…该程序需要不同的程序集(但宏可以解决该问题)。4。特定于编译器(可能在编译器之间移动时遇到问题)。我没试过,所以我真的不知道。


您需要一个常数表达式来声明C/C++中的数组。

对于动态大小数组,需要在堆上分配内存,然后管理该内存的提升时间。

1
2
3
4
5
void foo(int n) {
    int* values = new int[n]; //Declare a variable length array
    [...]
    delete [] values;
}