在过去的几年里我很少用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总是一个更好的解决方案。
- 只是出于好奇,为什么需要在堆栈上分配它?您是不是在处理堆分配性能问题?
- @dimitri并不是这样,但不可否认的是,堆栈分配将比堆分配更快。在某些情况下,这可能很重要。
- 可变长度数组的主要优点是所有数据都很接近,所以当您遍历这个数组时,您可以读取和写入相邻的字节。您的数据被提取到缓存中,CPU可以在不从内存中提取和发送字节的情况下使用它。
- 可变长度数组也可用于用静态常量变量替换预处理器常量。同样在C中,VLA没有其他选项,有时需要编写可移植的C/C++代码(与编译器兼容)。
- 顺便说一句,clang++允许VLA。
- 不公平的比较,对于"这里真正痛苦的表达",在C版本中没有任何等价物。尝试为VLA编写初始值设定项(提示:不可能)
- 只要类型是可构造的并且构造起来很便宜(像原语),那么您就可以定义一个合理的最大值,然后分配一个固定大小的数组。然后,在[0,n)范围内工作。
- 他们提出了C++ 14作为运行时大小的数组(必须在堆栈上分配,并且与C VLAS有一些不同),连同类模板EDCOX1×0(EDCOX1×1)到它们的原始数组,但是两者都被投票掉(后者被降级为TS)。显然,dynarray旨在与特殊的编译器魔力相结合,以便在堆栈上使用时,可以将其优化为与运行时大小的数组(至少在具有传统堆栈和堆设置的平台上)一样高效。我对细节不太熟悉。
- clang支持类作为std::experimental::dynarray,在头中,作为libc++的一部分。clang和gcc都支持C VLA,gcc(但不是clang)也允许初始化它们;请注意,这些都带有c限制,即在运行时对sizeof进行VLA评估。
- 关于GCC和Clang:我的建议是-Werror=vla(GCC:包含在规范文件中)。
(背景:我有一些实现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++中阻止使用原始数组,而我更多地关注元编程和类型系统的含义。我不知道他是否认为元编程/类型系统的含义已经解决、可以解决,或者仅仅是无趣。
- 我同意弗拉斯是错的。更广泛实施和更有用的alloca()应该在c99中被标准化。VLA是标准委员会在实施之前突然出现的情况,而不是相反的情况。
- 我没有意识到C允许VLA作为函数参数。我同意(因为这是值得的,因为我不写编译器)这是相当可怕的。另一方面,VLA作为局部变量对我来说确实很明智。当然,您可以溢出堆栈,但在任何时候使用局部变量时都必须考虑这一点,所以这里没有真正的新内容。而"sizeof"的东西-这不只是一个功能的价格吗?
- 可变改进型系统是一个伟大的补充IMO,你的子弹点没有违反常识。(1)C标准没有区分"编译时"和"运行时",因此这是一个没有问题的问题;(2)*是可选的,您可以(并且应该)编写int A[][n];(3)您可以使用类型系统而不实际声明任何VLA。例如,函数可以接受可变修改类型的数组,并且可以使用不同维度的非VLA二维数组调用它。不过,你在文章的后半部分提出了有效的观点。
- 当转换到GSL::SPAN<>时,类型参数变得特别明显。一个VLA(因为G++支持那些作为C++的扩展)在通过模板试图推断其大小时是没有意义的。
- 你知道,你描述的所有有用的东西都将继续存在,即使VLA被支持——它不会为VLA编译,只是为固定长度的数组。我并不反对你对VLA的保留,只是他们更多的是使用而不是拥有这个特性。对函数指针执行void *的reinterpret_casts也意味着意外的行为和可能的堆栈溢出,但这并不意味着不可能。
- "声明VLA意味着从堆栈中任意抽取一大块。这是一个有保证的堆栈溢出和崩溃。(每当您声明int a[n]时,您就隐式地断言您有2GB的堆栈可供使用"是经验错误的。我刚刚运行了一个VLA程序,其堆栈远小于2GB,没有任何堆栈溢出。
- @杰夫:在您的测试用例中,n的最大值是多少,堆栈的大小是多少?我建议您尝试为n输入一个至少与堆栈大小相同的值。(如果用户在程序中无法控制n的值,那么我建议您将n的最大值直接传播到声明中:declare int A[1000]或您需要的任何内容。只有当n的最大值不受任何小编译时间常数的限制时,VLA才是必需的,而且是危险的。)
- 根据我的经验,我喜欢VLA的主要原因是我可以索引二维数组,比如数组a[2][3]和memcpy(&a[1][2], ptr)。我从来没有真正关心过那个内存是在堆栈上还是在堆栈上。如果有一种方法可以将view作为任意形状的矩阵生成到某个缓冲区中,并像数组一样访问它,这就是我从VLA想要的一切。这意味着一个类,它获取一个指针,并提供操作符[]和&,仅用于句法结构。
- @MadScientist:该标准应该包括一个后进先出分配函数,其释放内部需要传递一个指针和早期分配的大小,一对标记/释放内部函数,它可以使用一个大于指针的东西[与va_列表一样],在那里释放将清除后进先出分配对象自相应的"标记",以及从已分配后进先出的函数返回的规范,可以在实现方便的情况下释放对象或不释放对象。
- @MadScientist:任何平台都可以支持这种语义[与alloca不同],分离标记/发布语义允许在堆上执行这种分配的实现在longjmp之后干净地恢复存储。
- 因为alloca()可以使用这样的内部函数实现,所以根据定义,alloca()可以作为编译器标准函数在任何平台上实现。编译器没有理由检测不到alloca()的第一个实例,也没有理由安排代码中嵌入的标记和发布类型,如果不能用堆栈实现alloca(),编译器也没有理由不能使用堆实现alloca()。硬的/不可移植的是在C编译器的基础上实现alloca(),这样它就可以跨许多编译器和操作系统工作。
- @MadScientist:我描述的语义可以在任何允许添加库和头文件的宿主编译器上得到支持。虽然任何平台的宿主编译器都可以处理alloca(),如果可以接受让longjmp放弃由其退出的函数所造成的任何阻塞,但我认为最好定义现有编译器可以支持的语义,并且可以与EDOCX1共存。〔5〕。
- 顺便说一下,如果要使用在指向teh数组本身的指针后传递数组大小参数的约定,可以使用k&r1函数语法,但不使用"现代"语法。
- 嗯,C++编译器通过允许VLAs作为扩展来解决这些问题。您还错过了有关小尺寸VLA和固定大小数组(等于最大VLA大小)之间的差异的要点——您必须默认地为静态数组构造所有元素,而且您可能无法!
- @谢尔盖:关于"你可能无法[默认构造A]的有趣评论。"是的:在一个特殊情况下,A vla[n]的长度动态确定为0,我们根本不需要调用A::A()。那么,编译器是否需要A::A()存在并可调用?Clang的VLA扩展名表示是;GCC的扩展名表示"有时"。godbolt.org/z/ddw4lx(并重新"解决了这些问题",请参见godbolt.org/z/guvxoo)。
- 毕竟,如果你知道"n在这里绝对小于1000",那么你只需声明int a[1000]。"是胡说八道。例如,如果在99.99%的函数调用上VLA的长度为10,在0.01%的调用上只达到其1000的上限,那么您基本上浪费了1000个字节,只要帧保留在堆栈上,就永远不会释放这些字节,如果e函数在控制流层次结构中处于较高的位置。您可能认为1000字节并不多,但每次CPU必须移入或移出该函数时,都会将所有缓存未命中因素考虑在内!
最近有一个关于在USENET中启动的讨论:为什么C++0x中没有VLAs。
我同意那些似乎同意必须在堆栈上创建一个潜在的大数组(通常只有很少的可用空间)的人的观点,这是不好的。参数是,如果事先知道大小,则可以使用静态数组。如果你事先不知道大小,你会写不安全的代码。
C99 VLAS可以提供一个小的好处,可以创建小数组而不浪费空间或调用未使用的元素的构造函数,但是它们会给类型系统带来相当大的变化(您需要根据运行时值来指定类型),除了EDCOX1·1操作符之外,在当前C++中还不存在这种类型。类型说明符,但它们是特殊处理的,这样运行时性就不会超出new运算符的范围。
您可以使用std::vector,但它并不完全相同,因为它使用动态内存,使用自己的堆栈分配器并不容易(对齐也是一个问题)。它也不能解决相同的问题,因为向量是一个可调整大小的容器,而VLA是固定大小的。C++动态数组建议旨在引入基于库的解决方案,作为基于语言的VLA的替代方案。但是,据我所知,它不会成为C++0X的一部分。
- +1并接受。不过,有一点需要说明,我认为安全性论证有点弱,因为有很多其他方法可以导致堆栈溢出。安全参数可以用来支持不应该使用递归的位置,并且应该从堆中分配所有对象。
- 所以你是说,因为有其他方法可以导致堆栈溢出,我们还是鼓励更多的方法吧?
- @安德烈亚斯,同意这个弱点。但是对于递归,需要大量的调用,直到堆栈被耗尽,如果可能的话,人们将使用迭代。不过,正如一些在usenet线程上的人所说,在所有情况下,这都不是反对VLA的理由,因为有时您肯定知道一个上限。但是在这些情况下,根据我所看到的,静态数组同样足够,因为它不会浪费太多空间(如果这样,那么您实际上需要询问堆栈区域是否再次足够大)。
- 也请看Matt Austern在线程中的答案:VC++的语言规范对于C++来说可能更为复杂,因为C++中更严格的类型匹配(例子:C允许在EcOx1 3)中分配EDCOX1的2度,这是不允许的,因为C++不知道"类型兼容性"——它需要精确的Matc。HES)、类型参数、异常、con-和析构函数以及资料。我不确定VLA的好处是否真的会抵消所有的工作。但是,我在现实生活中从来没有使用过VLA,所以我可能不知道它们的好用例。
- @约翰内斯:在某些情况下,使用VLA可能会克服你的异议。例如,递归函数,在每个步骤中,我都需要n_i的空间量。唯一的上限是所有n_i的和,不是任何单个n_i。所以我可能知道(a)s对我的堆栈足够小,所以vla不会溢出,但(b)s*递归的最大深度对我的堆栈来说太大。当然,这并不能证明一个复杂的语言特性是正确的:我应该只在递归顶部的堆栈上分配s,并向下传递一个指针,指向剩下未使用的内容。
- 啊,我现在看到kaz kylheku已经在com.std.c++线程上给出了这个例子。之后,alf给出的Windows字符串示例对我来说似乎是错误的,因为对于我来说,它对窄字符串函数的调用方承担了太多的责任,以确保有足够的堆栈留给库使用VLA。在这种字符串转换的情况下,VLA不会做你真正想要做的事情,也就是说,如果有空间,就在堆栈上进行分配;如果没有空间,就从堆上进行分配。但我猜在Windows上,如果堆上有空间,就在堆栈上有"空间",相当多。
- @约翰内斯:"能够在不浪费空间的情况下创建小数组,或为未使用的元素调用构造函数,这是一种好处。"一个Boost::类似数组的容器,它使用一个运行时参数来初始化分配的多少元素,这非常有用(除了递归算法中的可能)。任何运行时大小的数组都应该有最大数量的元素,并且立即分配这个数量的堆栈空间将使您更快地遇到无法避免的崩溃,这是很好的。
- @的确,维克多。但最大大小的数组也会调用所有构造函数,这可能不是您想要的。我认为类似数组的容器是在改变语言以引入VLA(我认为这是一个很大的改变)和完全不支持之间的一个很好的折衷方案。
- @约翰内斯:不,我的意思是,如果数组是一个包装基元类型(即无符号字符[n*size of(t)),数组可以调用placement new/constructor来获取runtime size变量中的元素数。(如果n表示静态最大大小,t表示类型,n表示运行时大小,则构造函数执行如下操作:(i…n)new(this+sizeof(t)*i)t();
- @维克多,我同意。这样做很有用,您可以提供N作为模板参数作为最大项目计数。编写C++0x,使用各种对齐工具(EDCOX1,1,等等)将更容易。
- 我想要VLA的主要原因是它们非常适合评估任意程度的多项式——我需要堆栈分配的速度,n的值不会很高——通常是2-4,所以设置任意最大值20是浪费的。我目前用一个杂乱的模板解决方案来实现这一点,但是VLA将更加适合。
- @ahelps:也许最适合的类型是一种类型,它的行为类似于vector,但需要固定的后进先出使用模式,并为每个线程保持一个或多个静态分配的缓冲区,这些缓冲区通常根据线程使用过的最大总分配来调整大小,但可以显式地进行调整。在通常情况下,正常的"分配"只需要指针复制、指针减法的指针、整数比较和指针加法;取消分配只需要指针复制。比VLA慢不了多少。
- 顺便说一下,在C++中,我推荐FoLy或Boost的Simple向量(GITHUB.COM/脸谱网/FLLY/BLB/MULTION/FLLY/DOCS/HELLIP)。愚蠢的版本还允许您禁止它使用堆。这用"足够大的堆栈数组"方法解决了不需要的构造函数调用的问题。
- @安德烈亚斯布林克说得很好。不是C++的方式,让程序员决定使用和安全使用它。
如果您愿意,可以在运行时使用alloca()在堆栈上分配内存:
1 2 3 4
| void foo (int n)
{
int *values = (int *)alloca(sizeof(int) * n);
} |
。
在堆栈上分配意味着当堆栈释放时,它将自动释放。
快速说明:正如MacOSX的alloca(3)手册页中所提到的,"alloca()函数依赖于机器和编译器,它的使用被取消了。"正如你所知道的。
- 此外,alloca()的作用域是整个函数,而不仅仅是包含变量的代码块。因此,在循环内部使用它将不断增加堆栈。VLA没有此问题。
- 但是,具有封闭块作用域的VLA意味着它们在整个函数的作用域方面远不如alloca()。考虑:if (!p) { p = alloca(strlen(foo)+1); strcpy(p, foo); }这不能用VLA来完成,这正是因为它们的块范围。
- 这并不能回答OP的"为什么"问题。此外,这是一个类似于C的解决方案,而不是真正的C++式解决方案。
在我自己的工作中,我意识到每当我想要变长自动数组或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。
- 有趣。Herb Sutter在这里的动态数组下讨论它:isocpp.org/blog/2013/04/trip-report-iso-c-spring-2013-meetin&zwnj;&8203;g(这是维基百科信息的参考)
- 2014年1月18日,维基百科上写道:"运行时大小的阵列和Dynarray已经被转移到了阵列扩展技术规范中"。:en.wikipedia.org/w/&hellip;
- 维基百科不是一个规范性的参考:这个提议并没有进入C++ 14。
- @M.M.会吗?
- 这个W.R.T.C++ 17的状态是什么?
- @不知道,使用boost::container::static_vector
这被认为包含在C++/1x中,但是被删除了(这是对我之前所说的一个修正)。
不管怎样,它在C++中都不那么有用,因为我们已经有了EDCOX1,0个来填充这个角色。
- 不,我们不,STD::向量不在堆栈上分配数据。:)
- "堆栈"是一个实现细节;只要满足对象生存期的保证,编译器就可以从任何地方分配内存。
- @M.M:够公平的,但实际上我们还是不能用std::vector代替,比如说alloca()。
- @在获得正确的程序输出方面,您可以。绩效是实施的质量问题
使用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向量并不总是更好的。经常,是的。总是,不需要。如果您只需要一个小数组,在堆空间很慢的平台上,并且您的库的矢量实现使用堆空间,那么这个特性如果存在的话可能会更好。
如果您在编译时知道该值,可以执行以下操作:
1 2 3 4 5 6
| template <int X>
void foo(void)
{
int values[X];
} |
编辑:您可以创建一个使用堆栈分配器(alloca)的向量,因为分配器是一个模板参数。
- 如果您在编译时知道这个值,那么根本不需要模板。只需在非模板函数中直接使用x。
- 有时调用者在编译时知道,而被调用者不知道,这就是模板的好处所在。当然,在一般情况下,直到运行时才知道x。
- 不能在stl分配器中使用alloca—堆栈帧被破坏时,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。特定于编译器(可能在编译器之间移动时遇到问题)。我没试过,所以我真的不知道。
- 以东十一〔0〕和拯救你的理智。
- …如果你想自己把这个卷起来,可以上个RAII课吗?
- 您可以简单地使用boost::container::static_vector you。
- 对于其他编译程序来说,它没有与MSVC具有更多原始程序集的等效项。VC可能会理解esp发生了变化,并将调整其对堆栈的访问,但在gcc中,您将完全破坏它——至少如果您使用优化,尤其是-fomit-frame-pointer的话。
您需要一个常数表达式来声明C/C++中的数组。
对于动态大小数组,需要在堆上分配内存,然后管理该内存的提升时间。
1 2 3 4 5
| void foo(int n) {
int* values = new int[n]; //Declare a variable length array
[...]
delete [] values;
} |
- 是的,我知道,问题是为什么这是C99而不是C++?
- c99[[email protected]/jtc1/sc22/wg14/www/docs/n1401.pdf]具有VLA(可变长度数组)。
- 最新的C99草案是http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf(3.7mb pdf);其中包括原始的C99标准和三个技术勘误。