在过去的几年里,我不怎么使用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
}
矢量版本变得相当笨拙:
void foo(int x, int y, int z) {
vector< vector< vector<int> > > values( /* Really painful expression here. */);
}
片、行和列也可能分布在整个内存中。
看看comp. stdc++上的讨论,很明显这个问题很有争议,争论双方都有一些重量级人物。当然,std::vector并不总是更好的解决方案。
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++中提供高效和优雅的解决方案。
(背景:我有一些实现C和c++编译器的经验。)
C99中的变长数组基本上是一个错误。为了支持VLAs, C99不得不根据常识作出以下让步:
sizeof x is no longer always a compile-time constant; the compiler must sometimes generate code to evaluate a sizeof-expression at runtime.
Allowing two-dimensional VLAs (int A[x][y]) required a new syntax for declaring functions that take 2D VLAs as parameters: void foo(int n, int A[][*]).
Less importantly in the C++ world, but extremely important for C's target audience of embedded-systems programmers, declaring a VLA means chomping an arbitrarily large chunk of your stack. This is a guaranteed stack-overflow and crash. (Anytime you declare int A[n], you're implicitly asserting that you have 2GB of stack to spare. After all, if you know "n is definitely less than 1000 here", then you would just declare int A[1000]. Substituting the 32-bit integer n for 1000 is an admission that you have no idea what the behavior of your program ought to be.)
好了,现在让我们开始讨论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++中真的有意义吗?
As every other answerer has already pointed out, C++ provides lots of heap-allocation mechanisms (std::unique_ptr<int[]> A = new int[n]; or std::vector<int> A(n); being the obvious ones) when you really want to convey the idea "I have no idea how much RAM I might need." And C++ provides a nifty exception-handling model for dealing with the inevitable situation that the amount of RAM you need is greater than the amount of RAM you have. But hopefully this answer gives you a good idea of why C99-style VLAs were not a good fit for C++ — and not really even a good fit for C99. ;)
有关该主题的更多信息,请参阅Bjarne Stroustrup 2013年10月关于VLAs的论文N3810“阵列扩展的替代方案”。Bjarne的POV与我的非常不同;N3810更侧重于为这些东西找到一个好的c++语法,并反对在c++中使用原始数组,而我更关注元编程和类型系统的含义。我不知道他是否认为元编程/类型系统的含义是已解决的、可解决的,还是仅仅是无趣的。
“合理使用可变长度数组”(Chris Wellons, 2019-10-27)是一篇很好的博客文章,触及了许多相同的观点。