【问题标题】:Why aren't variable-length arrays part of the C++ standard?为什么可变长度数组不是 C++ 标准的一部分?
【发布时间】:2021-12-31 12:51:02
【问题描述】:

在过去的几年里,我没有经常使用 C。今天在阅读this question 时,遇到了一些我不熟悉的 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 总是更好的解决方案并不明显。

【问题讨论】:

  • 只是出于好奇,为什么需要在栈上分配呢?你害怕堆分配性能问题吗?
  • @Dimitri 不是真的,但不可否认堆栈分配将比堆分配快。在某些情况下,这可能很重要。
  • 可变长度数组的主要优点是所有数据都靠得很近,因此当您遍历该数组时,您可以彼此相邻地读取和写入字节。您的数据被提取到缓存中,cpu 可以处理它而无需从内存中获取和发送字节。
  • 可变长度数组也可用于用静态常量变量替换预处理器常量。同样在 C 中,您没有其他 VLA 选项,有时需要编写可移植的 C/C++ 代码(与两种编译器兼容)。
  • 顺便说一句,clang++ 似乎允许 VLA。

标签: c++ arrays standards variable-length-array variable-length


【解决方案1】:

VLA 是更大的可变修饰类型家族的一部分。 这一系列类型非常特殊,因为它们具有 runtime 组件。

代码:

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

【讨论】:

    【解决方案2】:

    (背景:我有一些实现 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]。替换1000 的 32 位整数 n 承认您不知道程序的行为应该是什么。)

    好的,现在让我们开始讨论 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&lt;decltype(A1), decltype(A2)&gt;() 怎么办?在这种情况下,对myfunc 的调用甚至不应该编译,因为模板类型推导应该失败!我们怎么可能在运行时模拟这种行为?

    基本上,C++ 正朝着将越来越多的决策推入编译时 的方向发展:模板代码生成、constexpr 函数评估等等。同时,C99 忙于将传统的编译时决策(例如sizeof)推入运行时。考虑到这一点,花任何精力尝试将 C99 风格的 VLA 集成到 C++ 中真的有意义吗?

    正如所有其他回答者已经指出的那样,当您真的想传达“我不知道我有多少 RAM可能需要。” C++ 提供了一个漂亮的异常处理模型来处理不可避免的情况,即您需要的 RAM 量大于您拥有的 RAM 量。但希望 这个 答案能让您很好地了解为什么 C99 风格的 VLA 适合 C++,甚至不适合 C99。 ;)


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


    "Legitimate Use of Variable Length Arrays"(Chris Wellons,2019 年 10 月 27 日)是一篇很好的博文,其中提到了许多相同的点。

    【讨论】:

    • 我同意 VLA 是错误的。实现更广泛、更有用的alloca() 应该在 C99 中进行标准化。 VLA 是标准委员会在实施之前跳出来发生的事情,而不是相反。
    • 可变修改类型系统是 IMO 的一个很好的补充,您的任何要点都没有违反常识。 (1) C 标准不区分“编译时”和“运行时”,所以这不是问题; (2) * 是可选的,你可以(并且应该)写int A[][n]; (3) 您可以使用类型系统而无需实际声明任何 VLA。例如,一个函数可以接受可变修改类型的数组,并且可以使用不同维度的非 VLA 二维数组调用它。但是,您在帖子的后半部分提出了有效的观点。
    • "声明一个 VLA 意味着占用你的堆栈的任意大块。这是保证堆栈溢出和崩溃。(任何时候你声明 int A[n],你隐含断言你有2GB 的堆栈备用”在经验上是错误的。我刚刚运行了一个堆栈远小于 2GB 的 VLA 程序,没有任何堆栈溢出。
    • @Jeff:您的测试用例中n 的最大值是多少,您的堆栈大小是多少?我建议您尝试为n 输入一个至少与堆栈大小一样大的值。 (如果用户无法在您的程序中控制n 的值,那么我建议您将n 的最大值直接传播到声明中:声明int A[1000] 或您需要的任何内容。只有当n 的最大值不受任何小的编译时间常数的限制时,VLA 才是必要的,也是危险的。)
    • '毕竟,如果你知道“这里的n肯定小于1000”,那你就直接声明int A[1000]吧。'就是一派胡言。例如,如果 VLA 长度在 99.99% 的函数调用中为 10,并且仅在 0.01% 的调用中达到其上限 1000,那么您基本上浪费了 1000 个字节,只要框架保留在堆栈上——如果该函数在您的控制流层次结构中处于较高位置,这几乎可能一直存在。您可能认为 1000 字节并不多,但是每次您的 CPU 必须移入和移出该函数时,都要考虑所有缓存未命中!
    【解决方案3】:

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

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

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

    【讨论】:

    • 有趣。 Herb Sutter 在 Dynamic Arrays 下讨论了它:isocpp.org/blog/2013/04/trip-report-iso-c-spring-2013-meeting(这是维基百科信息的参考)
    • “运行时大小的数组和 dynarray 已移至 Array Extensions 技术规范”于 2014 年 1 月 18 日在 Wikipedia 上写道 78.86.152.103:en.wikipedia.org/w/…
    • 维基百科不是一个规范的参考 :) 这个提议没有进入 C++14。
    • @ViktorSehr:这个 w.r.t 的状态如何? C++17?
    • @einpoklum 不知道,使用 boost::container::static_vector
    【解决方案4】:

    最近在usenet 中开始了关于此的讨论:Why no VLAs in C++0x

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

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

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

    【讨论】:

    • +1 并被接受。不过有一条评论,我认为安全论点有点弱,因为有很多其他方法会导致堆栈溢出。安全参数可以用来支持你永远不应该使用递归并且你应该从堆中分配所有个对象的立场。
    • 那么你的意思是,因为还有其他方法会导致堆栈溢出,我们还不如鼓励更多?
    • @Andreas,同意这个弱点。但是对于递归,它需要大量的调用直到堆栈被吃掉,如果是这样的话,人们会使用迭代。但是,正如 usenet 线程上的一些人所说,这并不是在所有情况下都反对 VLA,因为有时您肯定知道上限。但在这些情况下,从我所见,静态数组同样就足够了,因为它无论如何都不会浪费太多空间(如果它,那么你实际上必须询问堆栈区域是否很大又够了)。
    • 还可以看看 Matt Austern 在那个线程中的回答:VLA 的语言规范对于 C++ 可能要复杂得多,因为 C++ 中的类型匹配更严格(例如:C 允许将 T(*)[] 分配给T(*)[N] - 在 C++ 中这是不允许的,因为 C++ 不知道“类型兼容性”——它需要完全匹配)、类型参数、异常、con-和析构函数以及其他东西。我不确定 VLA 的好处是否真的会为所有这些工作带来回报。但是,我从来没有在现实生活中使用过 VLA,所以我可能不知道它们的好用例。
    • @AHelps:也许最好的方法是一种行为有点像 vector 但需要固定的 LIFO 使用模式并维护一个或多个每个线程静态分配的缓冲区的类型通常根据线程曾经使用过的最大总分配来确定大小,但可以显式修剪。在常见情况下,正常的“分配”只需要指针复制、指针减法、整数比较和指针加法;取消分配只需要一个指针副本。不比 VLA 慢多少。
    【解决方案5】:

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

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

    【讨论】:

    • ...如果你想自己动手,也许可以使用 RAII 类?
    • 你可以简单地使用 boost::container::static_vector 。
    • 这没有其他编译器的等价物,它们比 MSVC 有更多的原始程序集。 VC 可能会理解 esp 已更改,并将调整其对堆栈的访问,但在例如GCC 你会完全破坏它——至少如果你使用优化,尤其是-fomit-frame-pointer
    【解决方案6】:

    这被考虑包含在 C++/1x 中,but was dropped(这是对我之前所说的更正)。

    无论如何,它在 C++ 中的用处不大,因为我们已经有 std::vector 来填补这个角色。

    【讨论】:

    • 不,我们没有,std::vector 不在堆栈上分配数据。 :)
    • @M.M:很公平,但实际上我们仍然不能使用std::vector 来代替alloca()
    • @einpoklum 在为您的程序获得正确输出方面,您可以。性能是一个实施质量问题
    • @M.M 实施质量不可移植。如果你不需要性能,你一开始就不要使用c++
    • 如何处理带有向量的多维而不需要繁琐的乘法。 C++ 只是提供了排除有用工具的借口,而他们却谎称“我们需要允许人们编写我们无法想象的东西”。如果这没用,为什么所有这些语言都支持它:en.wikipedia.org/wiki/Variable-length_array 甚至 C# 也添加了它,是的,它被称为 stackalloc....
    【解决方案7】:

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

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

    【讨论】:

    • 这有必须手动管理堆栈的缺点,但它通常是一个非常好的方法。
    【解决方案8】:

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

    【讨论】:

    • 您能否提供指向您所指向的标准段落的链接?
    【解决方案9】:

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

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

    【讨论】:

      【解决方案10】:

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

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

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

      【讨论】:

      • 可变长度数组的一个主要应用是计算任意次数多项式。在这种情况下,您的“小的性能缺陷”意味着“在典型情况下,代码运行速度要慢五倍”。这可不小。
      • 为什么不直接使用std::vector&lt;int&gt; values(n);?通过在构造后使用resize,您将禁止不可移动的类型。
      • 不等价。脏语法。
      【解决方案11】:

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

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

      【讨论】:

      • -1 矢量并不总是更好。通常,是的。总是,没有。如果你只需要一个小数组,在一个堆空间很慢的平台上,并且你的库的向量实现使用堆空间,那么如果它存在这个特性可能会更好。
      【解决方案12】:

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

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

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

      【讨论】:

      • 如果你在编译时就知道值,你根本不需要模板。只需在非模板函数中直接使用 X。
      • 有时调用者在编译时知道而被调用者不知道,这就是模板的好处。当然,在一般情况下,直到运行时才有人知道 X。
      • 你不能在 STL 分配器中使用 alloca - 当堆栈帧被销毁时,从 alloca 分配的内存将被释放 - 这就是应该分配内存的方法返回的时候。
      【解决方案13】:

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

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

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

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

      【讨论】:

      • 另外,alloca() 的作用域是整个函数,而不仅仅是包含变量的代码块。因此,在循环中使用它会不断增加堆栈。 VLA 没有这个问题。
      • 但是,具有封闭块范围的 VLA 意味着它们的用处远不如具有整个函数范围的 alloca()。考虑:if (!p) { p = alloca(strlen(foo)+1); strcpy(p, foo); } VLA 无法做到这一点,正是因为它们的块范围。
      • 这不能回答 OP 的 why 问题。此外,这是一个类似C 的解决方案,而不是真正的C++-ish。
      • 不等价。 alloca 的语法很脏。
      猜你喜欢
      • 2011-11-16
      • 1970-01-01
      • 2011-02-06
      • 2021-04-02
      • 2013-03-31
      • 2014-03-04
      • 1970-01-01
      相关资源
      最近更新 更多