409

在过去的几年里,我没有经常使用 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++来看,很明显这个问题是相当有争议的,争论双方都有一些非常重量级的名字。当然,astd::vector总是更好的解决方案并不明显。

4

13 回答 13

304

(背景:我有一些实现 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 没有的方式依赖它。例如:

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

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

那这个呢:

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 在这种情况下,甚至不应该调用compile ,因为模板类型推导应该失败!我们怎么可能在运行时模拟这种行为?

基本上,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。;)


有关该主题的更多信息,请参阅Bjarne Stroustrup 2013 年 10 月关于 VLA 的论文N3810 “Alternatives for Array Extensions” 。Bjarne 的 POV 和我的很不一样;N3810 更侧重于为事物找到一种好的 C++语法,并反对在 C++ 中使用原始数组,而我更侧重于对元编程和类型系统的影响。我不知道他是否认为元编程/类型系统的影响已解决、可解决或仅仅是无趣。


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

于 2014-02-03T03:01:16.997 回答
239

最近在 usenet 上开始了一个关于这个的讨论:Why no VLAs in C++0x

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

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

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

于 2009-12-11T10:28:54.903 回答
30

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

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

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

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

于 2009-12-11T10:31:23.570 回答
20

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

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

于 2013-03-21T17:05:05.303 回答
15

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

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

于 2011-01-22T19:33:36.713 回答
13

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

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

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

于 2013-08-13T10:40:51.410 回答
7

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

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

于 2009-12-11T10:26:33.647 回答
2

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

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

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

于 2009-12-11T10:22:57.803 回答
2

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

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

于 2009-12-11T10:25:33.430 回答
1

C99 允许 VLA。它对如何声明 VLA 设置了一些限制。详见标准6.7.5.2。C++ 不允许 VLA。但是 g++ 允许这样做。

于 2012-07-31T05:56:39.070 回答
0

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++ 中提供高效和优雅的解决方案。

于 2021-09-16T22:12:48.117 回答
-3

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

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

}

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

于 2009-12-11T10:26:49.663 回答
-8

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

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 位中不起作用build... 需要不同的程序集(但宏可能会解决该问题)。4. 编译器特定(在编译器之间移动可能有问题)。我没试过所以我真的不知道。

于 2014-01-15T08:40:03.757 回答