提问人:Andreas Brinck 提问时间:12/11/2009 最后编辑:Jan SchultkeAndreas Brinck 更新时间:9/26/2023 访问量:205029
为什么可变长度数组不是 C++ 标准的一部分?
Why aren't variable-length arrays part of the C++ standard?
问:
在过去的几年里,我很少使用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
答:
为此,请使用 std::vector。例如:
std::vector<int> values;
values.resize(n);
内存将在堆上分配,但这只有一个很小的性能缺点。此外,明智的做法是不要在堆栈上分配大型数据块,因为它的大小相当有限。
评论
std::vector<int> values(n);
resize
像这样的数组是 C99 的一部分,但不是标准 C++ 的一部分。正如其他人所说,向量总是一个更好的解决方案,这可能就是为什么可变大小的数组不在 C++ 标准(或建议的 C++0x 标准中)的原因。
顺便说一句,对于“为什么”C++标准是这样的问题,主持的Usenet新闻组comp.std.c++是去的地方。
评论
std::vector
这被考虑包含在 C++/1x 中,但被删除了(这是对我之前所说的更正)。
无论如何,它在 C++ 中用处不大,因为我们已经必须填补这个角色。std::vector
评论
std::vector
alloca()
最近在usenet上有一个关于这个问题的讨论:为什么C++0x中没有VLA。
我同意那些似乎同意在堆栈上创建一个潜在的大型数组的人,这通常只有很少的可用空间,这并不好。参数是,如果你事先知道大小,你可以使用静态数组。如果你事先不知道大小,你就会写出不安全的代码。
C99 VLA 可以提供一个小好处,即能够在不浪费空间或为未使用的元素调用构造函数的情况下创建小数组,但它们会给类型系统带来相当大的更改(您需要能够根据运行时值指定类型 - 这在当前的 C++ 中尚不存在,除了运算符类型说明符, 但是它们被特殊处理,因此运行时性不会超出运算符的范围。new
new
您可以使用 ,但它并不完全相同,因为它使用动态内存,并且让它使用自己的堆栈分配器并不容易(对齐也是一个问题)。它也不能解决同样的问题,因为向量是一个可调整大小的容器,而 VLA 是固定大小的。C++ 动态数组提案旨在引入基于库的解决方案,作为基于语言的 VLA 的替代方案。但是,据我所知,它不会成为 C++0x 的一部分。std::vector
评论
T(*)[]
T(*)[N]
vector
如果您愿意,您可以随时使用 alloca() 在运行时在堆栈上分配内存:
void foo (int n)
{
int *values = (int *)alloca(sizeof(int) * n);
}
在堆栈上分配意味着当堆栈展开时,它将自动释放。
快速说明:正如 Mac OS X 手册页中提到的 alloca(3) ,“alloca() 函数依赖于机器和编译器;不鼓励使用它。只是为了让你知道。
评论
if (!p) { p = alloca(strlen(foo)+1); strcpy(p, foo); }
C
C++
在某些情况下,与执行的操作相比,分配堆内存的成本非常高。一个例子是矩阵数学。如果你使用小矩阵,比如说 5 到 10 个元素,并做大量的算术运算,那么 malloc 开销将非常大。同时,将大小作为编译时常量似乎非常浪费且不灵活。
我认为 C++ 本身非常不安全,以至于“尽量不添加更多不安全功能”的论点不是很强。另一方面,由于 C++ 可以说是运行时效率最高的编程语言,因此它总是有用的:编写性能关键型程序的人将在很大程度上使用 C++,并且他们需要尽可能多的性能。将东西从堆移动到堆栈就是这样一种可能性。减少堆块的数量是另一回事。允许 VLA 作为对象成员是实现此目的的一种方法。我正在研究这样的建议。诚然,实现起来有点复杂,但似乎完全可行。
在我自己的工作中,我意识到,每次我想要像可变长度自动数组或 alloca() 这样的东西时,我并不真正关心内存是否物理位于 cpu 堆栈上,只是它来自某个堆栈分配器,它不会导致缓慢的到通用堆。因此,我有一个每个线程对象,它拥有一些内存,它可以从中推送/弹出可变大小的缓冲区。在某些平台上,我允许它通过 mmu 增长。其他平台具有固定大小(通常也伴随着固定大小的 cpu 堆栈,因为没有 mmu)。我使用的一个平台(掌上游戏机)无论如何都有宝贵的小 cpu 堆栈,因为它驻留在稀缺的快速内存中。
我并不是说永远不需要将可变大小的缓冲区推送到 cpu 堆栈上。老实说,当我发现这不是标准时,我感到很惊讶,因为这个概念似乎很适合语言。不过,对我来说,“可变大小”和“必须物理位于 cpu 堆栈上”的要求从未一起出现过。这与速度有关,所以我制作了自己的“数据缓冲区并行堆栈”。
评论
thread_local std::pmr::unsynchronized_pool_resource;
似乎它将在 C++14 中可用:
https://en.wikipedia.org/wiki/C%2B%2B14#Runtime-sized_one_dimensional_arrays
更新:它没有进入 C++14。
评论
(背景:我有一些实现 C 和 C++ 编译器的经验。
C99 中的可变长度数组基本上是一个失误。为了支持VLA,C99必须对常识做出以下让步:
sizeof x
不再始终是编译时常量;编译器有时必须生成代码才能在运行时计算 -expression。sizeof
允许二维 VLA () 需要一种新的语法来声明将 2D VLA 作为参数的函数:。
int A[x][y]
void foo(int n, int A[][*])
在 C++ 世界中不太重要,但对于 C 的目标受众嵌入式系统程序员来说极为重要,声明 VLA 意味着占用任意大块堆栈。这是有保证的堆栈溢出和崩溃。(每当您声明 时,您都隐含地断言您有 2GB 的堆栈可用。毕竟,如果你知道“这里肯定少于1000”,那么你只会声明.将 32 位整数替换为 是承认您不知道程序的行为应该是什么。
int A[n]
n
int A[1000]
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++ 正朝着将越来越多的决策推向编译时的方向发展:模板代码生成、函数评估等等。与此同时,C99 正忙于将传统的编译时决策(例如 )推入运行时。考虑到这一点,花费任何精力尝试将 C99 样式的 VLA 集成到 C++ 中真的有意义吗?constexpr
sizeof
正如其他所有回答者已经指出的那样,当你真的想传达“我不知道我可能需要多少RAM”的想法时,C++ 提供了许多堆分配机制(或者是显而易见的机制)。C++ 提供了一个漂亮的异常处理模型,用于处理不可避免的情况,即您需要的 RAM 量大于您拥有的 RAM 量。但希望这个答案能让你很好地了解为什么 C99 风格的 VLA 不适合 C++——甚至不适合 C99。;)std::unique_ptr<int[]> A = new int[n];
std::vector<int> A(n);
有关该主题的更多信息,请参阅 N3810“阵列扩展的替代方案”,Bjarne Stroustrup 2013 年 10 月关于 VLA 的论文。Bjarne 的 POV 与我的 POV 非常不同;N3810 更侧重于为事物找到一个好的 C++ish 语法,并阻止在 C++ 中使用原始数组,而我更关注对元编程和类型系统的影响。我不知道他是否认为元编程/类型系统的影响已经解决、可以解决,或者只是无趣。
一篇很好的博客文章是“可变长度数组的合法使用”(Chris Wellons,2019-10-27)。
评论
alloca()
*
int A[][n]
10
1000
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++ 中提供高效和优雅的解决方案。
评论