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

在过去的几年里,我不怎么使用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并不总是更好的解决方案。

192280 次浏览

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

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

内存将在堆上分配,但这只会带来很小的性能缺陷。此外,不要在堆栈上分配大数据锁是明智的,因为它的大小相当有限。

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

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

这被考虑包含在c++ /1x, 但是被放弃了中(这是对我前面所说的更正)。

无论如何,它在c++中都没有那么有用,因为我们已经有std::vector来填充这个角色。

最近在usenet: 为什么c++ 0x中没有vla上有一个关于这个的讨论。

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

C99 VLAs可以在不浪费空间或调用未使用元素的构造函数的情况下创建小型数组,但它们将对类型系统带来相当大的更改(你需要能够根据运行时值指定类型-目前c++中还不存在这种情况,除了new操作符类型说明符,但它们经过特殊处理,因此运行时性不会逃脱new操作符的作用域)。

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

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

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

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

注意:正如在Mac OS X的alloca(3)手册中提到的,“alloca()函数依赖于机器和编译器;不鼓励使用它。”告诉你一声。

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

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

在我自己的工作中,我已经意识到,每次我想要像变长自动数组或alloca()这样的东西时,我并不真正关心内存在cpu堆栈上的物理位置,只是它来自于一些堆栈分配器,它不会导致对一般堆的缓慢旅行。所以我有一个每个线程对象,它拥有一些内存,它可以推/弹出可变大小的缓冲区。在某些平台上,我允许通过mmu来发展。其他平台有固定大小(通常也有固定大小的cpu堆栈,因为没有mmu)。我所使用的一个平台(手持游戏主机)的cpu堆栈非常少,因为它位于稀缺且快速的内存中。

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

似乎c++ 14中也可以使用:

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

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

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

C99中的变长数组基本上是一个错误。为了支持VLAs, C99不得不根据常识作出以下让步:

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

  • 允许二维VLAs (int A[x][y])需要一种新的语法来声明以2D VLAs作为参数的函数: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;

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

那么这个呢:

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

编译器必须为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风格的vla 非常适合c++——甚至不适合C99。;)


有关该主题的更多信息,请参阅N3810“数组扩展的替代方案”, Bjarne Stroustrup 2013年10月关于VLAs的论文。Bjarne的POV与我的非常不同;N3810更侧重于为这些东西找到一个好的c++风格的语法,并反对在c++中使用原始数组,而我更侧重于元编程和类型系统的含义。我不知道他是否认为元编程/类型系统的含义是已解决的、可解决的,还是仅仅是无趣的。


可变长度数组的合法使用 (Chris Wellons, 2019-10-27)是一篇很好的博客文章,触及了许多相同的观点。

VLAs是一个更大的可变修改类型家族的一部分。 这类类型非常特殊,因为它们有运行时组件

代码:

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);

消除VLAs只能在堆栈上分配的流行的神话

回到刚才的问题。

这个运行时组件不能很好地与类型演绎一起工作,而类型演绎是c++类型系统的基础之一。它不可能使用模板,演绎和重载。

c++类型系统是静态的,所有类型必须在编译期间完全定义或推导。 虚拟机类型仅在程序执行期间完成。 在已经非常复杂的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++中提供高效和优雅的解决方案。