¿Por qué las matrices de longitud variable no forman parte del estándar C++?

Resuelto Andreas Brinck asked hace 15 años • 10 respuestas

No he usado mucho C en los últimos años. Cuando leí esta pregunta hoy me encontré con una sintaxis de C con la que no estaba familiarizado.

Aparentemente en C99 la siguiente sintaxis es válida:

void foo(int n) {
    int values[n]; //Declare a variable length array
}

Esta parece una característica bastante útil. ¿Hubo alguna vez una discusión sobre agregarlo al estándar C++ y, de ser así, por qué se omitió?

Algunas posibles razones:

  • Difícil de implementar para los proveedores de compiladores
  • Incompatible con alguna otra parte del estándar.
  • La funcionalidad se puede emular con otras construcciones de C++.

El estándar C++ establece que el tamaño de la matriz debe ser una expresión constante (8.3.4.1).

Sí, por supuesto, me doy cuenta de que en el ejemplo del juguete se podría usar std::vector<int> values(m);, pero esto asigna memoria del montón y no de la pila. Y si quiero una matriz multidimensional como:

void foo(int x, int y, int z) {
    int values[x][y][z]; // Declare a variable length array
}

la vectorversión se vuelve bastante torpe:

void foo(int x, int y, int z) {
    vector< vector< vector<int> > > values( /* Really painful expression here. */);
}

Los sectores, filas y columnas también se distribuirán potencialmente por toda la memoria.

Al observar la discusión, comp.std.c++queda claro que esta pregunta es bastante controvertida con algunos nombres muy importantes en ambos lados del argumento. Ciertamente no es obvio que a std::vectorsea siempre una mejor solución.

Andreas Brinck avatar Dec 11 '09 17:12 Andreas Brinck
Aceptado

(Antecedentes: tengo algo de experiencia en la implementación de compiladores de C y C++).

Las matrices de longitud variable en C99 fueron básicamente un paso en falso. Para apoyar los VLA, el C99 tuvo que hacer las siguientes concesiones al sentido común:

  • sizeof xya no es siempre una constante en tiempo de compilación; En ocasiones, el compilador debe generar código para evaluar una sizeofexpresión en tiempo de ejecución.

  • Permitir VLA bidimensionales ( int A[x][y]) requería una nueva sintaxis para declarar funciones que toman VLA 2D como parámetros: void foo(int n, int A[][*]).

  • Lo que es menos importante en el mundo de C++, pero extremadamente importante para el público objetivo de programadores de sistemas integrados de C, declarar un VLA significa consumir una porción arbitrariamente grande de su pila. Esto es un desbordamiento y falla de la pila garantizados . (Cada vez que declara int A[n], está afirmando implícitamente que tiene 2 GB de pila de sobra. Después de todo, si sabe que " ndefinitivamente hay menos de 1000 aquí", entonces simplemente declararía int A[1000]. Sustituir el entero de 32 bits npor 1000es una admisión que no tienes idea de cuál debería ser el comportamiento de tu programa).

Bien, pasemos ahora a hablar de C++. En C++, tenemos la misma fuerte distinción entre "sistema de tipos" y "sistema de valores" que tiene C89... pero realmente hemos comenzado a confiar en él de maneras que C no lo hacía. Por ejemplo:

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

Si nno fuera una constante en tiempo de compilación (es decir, si Afuera de un tipo modificado de forma variable), ¿cuál sería el tipo de S? ¿ SEl tipo de 's también se determinaría solo en tiempo de ejecución?

¿Qué pasa con esto?

template<typename T> bool myfunc(T& t1, T& t2) { ... };
int A1[n1], A2[n2];
myfunc(A1, A2);

El compilador debe generar código para alguna instanciación de myfunc. ¿Cómo debería verse ese código? ¿Cómo podemos generar estáticamente ese código, si no sabemos el tipo A1en el momento de la compilación?

Peor aún, ¿qué pasa si en tiempo de ejecución resulta que n1 != n2, entonces !std::is_same<decltype(A1), decltype(A2)>()? En ese caso, la llamada a myfunc ni siquiera debería compilarse , ¡porque la deducción del tipo de plantilla debería fallar! ¿Cómo podríamos emular ese comportamiento en tiempo de ejecución?

Básicamente, C++ se está moviendo en la dirección de impulsar cada vez más decisiones en tiempo de compilación : generación de código de plantilla, constexprevaluación de funciones, etc. Mientras tanto, C99 estaba ocupado impulsando decisiones tradicionalmente en tiempo de compilaciónsizeof (p. ej. ) al tiempo de ejecución . Con esto en mente, ¿realmente tiene sentido dedicar algún esfuerzo a intentar integrar VLA de estilo C99 en C++?

Como ya han señalado todos los demás encuestados, C++ proporciona muchos mecanismos de asignación de montón ( std::unique_ptr<int[]> A = new int[n];o std::vector<int> A(n);los más obvios) cuando realmente quieres transmitir la idea "No tengo idea de cuánta RAM podría necesitar". Y C++ proporciona un ingenioso modelo de manejo de excepciones para lidiar con la inevitable situación de que la cantidad de RAM que necesitas es mayor que la cantidad de RAM que tienes. Pero es de esperar que esta respuesta le dé una buena idea de por qué los VLA estilo C99 no eran una buena opción para C++, y ni siquiera eran una buena opción para C99. ;)


Para obtener más información sobre el tema, consulte N3810 "Alternatives for Array Extensions" , artículo de Bjarne Stroustrup de octubre de 2013 sobre VLA. El punto de vista de Bjarne es muy diferente al mío; N3810 se centra más en encontrar una buena sintaxis tipo C++ para las cosas y en desalentar el uso de matrices sin formato en C++, mientras que yo me centré más en las implicaciones para la metaprogramación y el sistema de tipos. No sé si considera que las implicaciones de la metaprogramación/sistema de tipos están resueltas, tienen solución o simplemente no son interesantes.


Una buena publicación de blog que aborda muchos de estos mismos puntos es "Uso legítimo de matrices de longitud variable" (Chris Wellons, 27 de octubre de 2019).

Quuxplusone avatar Feb 03 '2014 03:02 Quuxplusone

Recientemente se inició una discusión sobre esto en Usenet: ¿ Por qué no hay VLA en C++0x ?

Estoy de acuerdo con aquellas personas que parecen estar de acuerdo en que no es bueno tener que crear una gran matriz potencial en la pila, que generalmente tiene poco espacio disponible. El argumento es que, si conoce el tamaño de antemano, puede utilizar una matriz estática. Y si no conoce el tamaño de antemano, escribirá código inseguro.

Los VLA C99 podrían proporcionar un pequeño beneficio al poder crear matrices pequeñas sin desperdiciar espacio o llamar a constructores para elementos no utilizados, pero introducirán cambios bastante grandes en el sistema de tipos (debe poder especificar tipos dependiendo de los valores del tiempo de ejecución; esto todavía no existe en C++ actual, excepto los newespecificadores de tipo de operador, pero se tratan de manera especial, de modo que el tiempo de ejecución no escape al alcance del newoperador).

Puede usar std::vector, pero no es exactamente lo mismo, ya que usa memoria dinámica y hacer que use su propio asignador de pila no es exactamente fácil (la alineación también es un problema). Tampoco resuelve el mismo problema, porque un vector es un contenedor de tamaño variable, mientras que los VLA son de tamaño fijo. La propuesta de C++ Dynamic Array pretende introducir una solución basada en biblioteca, como alternativa a un VLA basado en lenguaje. Sin embargo, hasta donde yo sé, no será parte de C++ 0x.

Johannes Schaub - litb avatar Dec 11 '2009 10:12 Johannes Schaub - litb

Siempre puedes usar alloca() para asignar memoria en la pila en tiempo de ejecución, si lo deseas:

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

Estar asignado en la pila implica que se liberará automáticamente cuando la pila se desenrolle.

Nota rápida: como se menciona en la página de manual de Mac OS X para alloca(3), "La función alloca() depende de la máquina y del compilador; se desaconseja su uso". Solo para que sepas.

PfhorSlayer avatar Dec 11 '2009 10:12 PfhorSlayer

En mi propio trabajo, me di cuenta de que cada vez que quería algo como matrices automáticas de longitud variable o alloca(), realmente no me importaba que la memoria estuviera ubicada físicamente en la pila de la CPU, solo que viniera de algún asignador de pila que no provocara viajes lentos al montón general. Entonces tengo un objeto por subproceso que posee algo de memoria desde la cual puede insertar/explotar búferes de tamaño variable. En algunas plataformas permito que esto crezca a través de mmu. Otras plataformas tienen un tamaño fijo (generalmente acompañado de una pila de CPU de tamaño fijo porque no tienen mmu). De todos modos, una plataforma con la que trabajo (una consola de juegos portátil) tiene muy poca CPU porque reside en una memoria escasa y rápida.

No estoy diciendo que nunca sea necesario insertar buffers de tamaño variable en la pila de la CPU. Honestamente, me sorprendió cuando descubrí que esto no era estándar, ya que ciertamente parece que el concepto encaja bastante bien en el lenguaje. Sin embargo, para mí, los requisitos "tamaño variable" y "deben estar ubicados físicamente en la pila de la CPU" nunca se han cumplido. Se trataba de velocidad, así que hice mi propia especie de "pila paralela para buffers de datos".

Eric avatar Mar 21 '2013 17:03 Eric

Parece que estará disponible en C++14:

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

Actualización: no llegó a C++ 14.

Viktor Sehr avatar Aug 13 '2013 10:08 Viktor Sehr

Se consideró su inclusión en C++/1x, pero se eliminó (esta es una corrección de lo que dije antes).

De todos modos, sería menos útil en C++ ya que ya tenemos std::vectorque cumplir este rol.

philsquared avatar Dec 11 '2009 10:12 philsquared