По не совсем понятным мне причинам почти каждый раз, когда в обсуждении всплывает тема C99 VLA, люди начинают говорить преимущественно о возможности объявления массивов run-time size локальными объектами (т.е. создания их «на стеке "). Это довольно удивительно и вводит в заблуждение, поскольку этот аспект функциональности VLA — поддержка объявлений локальных массивов — оказывается скорее вспомогательной, вторичной возможностью, предоставляемой VLA. На самом деле это не играет существенной роли в том, что может сделать VLA. В большинстве случаев вопрос о местных декларациях VLA и сопутствующих им потенциальных ловушках выдвигается на передний план критиками VLA, которые используют его как «соломенное чучело», предназначенное для того, чтобы сорвать дискуссию и увязнуть в маловажных деталях.
Суть поддержки VLA в C заключается, прежде всего, в революционном качественном расширении языковой концепции type. Он предполагает введение таких принципиально новых видов типов, как вариабельно изменяемые типы. Практически каждая важная деталь реализации, связанная с VLA, на самом деле привязана к его типу, а не к объекту VLA как таковому. Именно введение в язык вариабельно изменяемых типов и составляет основную часть пресловутого пирога VLA, а возможность объявлять объекты таких типов в локальной памяти — не более чем незначительная и справедливо бесполезная вишенка на торте.
Подумайте об этом: каждый раз, когда кто-то объявляет что-то подобное в своем коде
/* Block scope */
int n = 10;
...
typedef int A[n];
...
n = 5; /* <- Does not affect `A` */
связанные с размером характеристики изменяемого типа A
(например, значение n
) завершаются в тот самый момент, когда управление проходит через вышеприведенное объявление typedef. Любые изменения значения n
, сделанные дальше по строке (ниже этого объявления A
), не влияют на размер A
. Остановитесь на секунду и подумайте, что это значит. Это означает, что реализация должна связать с A
скрытую внутреннюю переменную, которая будет хранить размер типа массива. Эта скрытая внутренняя переменная инициализируется из n
во время выполнения, когда управление переходит к объявлению A
.
Это придает вышеприведенному объявлению typedef довольно интересное и необычное свойство, чего мы раньше не видели: это объявление typedef генерирует исполняемый код (!). Более того, он генерирует не просто исполняемый код, а критически важный исполняемый код. Если мы каким-то образом забудем инициализировать внутреннюю переменную, связанную с таким объявлением typedef, мы получим «сломанный»/неинициализированный псевдоним typedef. Важность этого внутреннего кода является причиной того, что язык накладывает некоторые необычные ограничения на такие вариабельно изменяемые объявления: язык запрещает передачу управления в их область видимости из-за пределов их области видимости.
/* Block scope */
int n = 10;
goto skip; /* Error: invalid goto */
typedef int A[n];
skip:;
Еще раз обратите внимание, что приведенный выше код не определяет никаких массивов VLA. Он просто объявляет, казалось бы, невинный псевдоним для вариабельно изменяемого типа. Тем не менее, недопустимо перепрыгивать через такое объявление typedef. (Мы уже знакомы с такими ограничениями, связанными с переходом, в C++, хотя и в других контекстах).
Генерирующий код typedef
, typedef
, требующий инициализации во время выполнения, является значительным отклонением от того, что typedef
есть в "классическом" языке. (Это также представляет собой серьезное препятствие на пути внедрения VLA в C++.)
Когда кто-то объявляет фактический объект VLA, в дополнение к выделению фактической памяти массива компилятор также создает одну или несколько скрытых внутренних переменных, которые содержат размеры рассматриваемого массива. Нужно понимать, что эти скрытые переменные связаны не с самим массивом, а скорее с его вариабельно изменяемым типом.
Одним из важных и примечательных следствий такого подхода является следующее: дополнительная информация о размере массива, связанная с VLA, не встраивается непосредственно в объектное представление VLA. На самом деле он хранится помимо массива как данные "sidecar". Это означает, что объектное представление (возможно, многомерного) VLA полностью совместимо с объектным представлением обычного классического массива размера времени компиляции той же размерности и тех же размеров. Например
void foo(unsigned n, unsigned m, unsigned k, int a[n][m][k]) {}
void bar(int a[5][5][5]) {}
int main(void)
{
unsigned n = 5;
int vla_a[n][n][n];
bar(a);
int classic_a[5][6][7];
foo(5, 6, 7, classic_a);
}
Оба вызова функций в приведенном выше коде абсолютно корректны, и их поведение полностью определяется языком, несмотря на то, что мы передаем VLA там, где ожидается «классический» массив, и наоборот. Конечно, компилятор не может контролировать совместимость типов в таких вызовах (поскольку по крайней мере один из задействованных типов имеет размер времени выполнения). Однако при желании компилятор (или пользователь) имеет все необходимое для выполнения динамической проверки в отладочной версии кода.
(Примечание. Как обычно, параметры типа массива всегда неявно настраиваются в параметры типа указателя. Это относится к объявлениям параметров VLA точно так же, как и к «классическим» объявлениям параметров массива. Это означает, что в Параметр приведенного выше примера a
на самом деле имеет тип int (*)[m][k]
. На этот тип не влияет значение n
. Я намеренно добавил в массив несколько дополнительных измерений, чтобы сохранить его зависимость от значений времени выполнения.)
Совместимость между VLA и "классическими" массивами как параметрами функций также поддерживается тем фактом, что компилятору не нужно сопровождать вариабельно изменяемый параметр какой-либо дополнительной скрытой информацией о его размере. Вместо этого синтаксис языка заставляет пользователя передавать эту дополнительную информацию в открытом виде. В приведенном выше примере пользователь был вынужден сначала включить параметры n
, m
и k
в список параметров функции. Без объявления n
, m
и k
пользователь не смог бы объявить a
(см. также примечание выше о n
). Эти параметры, явно переданные в функцию пользователем, принесут информацию о фактических размерах a
.
В качестве другого примера, воспользовавшись поддержкой VLA, мы можем написать следующий код
#include <stdio.h>
#include <stdlib.h>
void init(unsigned n, unsigned m, int a[n][m])
{
for (unsigned i = 0; i < n; ++i)
for (unsigned j = 0; j < m; ++j)
a[i][j] = rand() % 100;
}
void display(unsigned n, unsigned m, int a[n][m])
{
for (unsigned i = 0; i < n; ++i)
for (unsigned j = 0; j < m; ++j)
printf("%2d%s", a[i][j], j + 1 < m ? " " : "\n");
printf("\n");
}
int main(void)
{
int a1[5][5] = { 42 };
display(5, 5, a1);
init(5, 5, a1);
display(5, 5, a1);
unsigned n = rand() % 10 + 5, m = rand() % 10 + 5;
int (*a2)[n][m] = malloc(sizeof *a2);
init(n, m, *a2);
display(n, m, *a2);
free(a2);
}
Этот код призван привлечь ваше внимание к следующему факту: этот код активно использует ценные свойства вариабельно изменяемых типов. Элегантно реализовать без VLA невозможно. Это основная причина, по которой эти свойства отчаянно необходимы в C, чтобы заменить уродливые хаки, которые использовались вместо них ранее. Но в то же время в указанной выше программе в локальной памяти не создается ни одного VLA, а это означает, что этот популярный вектор критики VLA вообще неприменим к этому коду.
По сути, два последних приведенных выше примера — это краткая иллюстрация того, в чем смысл поддержки VLA.
person
AnT
schedule
12.01.2019
alloca
не входит в стандарт. И VLA стал необязательным в C11. И то, и другое небезопасно, но большие массивы постоянного размера времени компиляции также небезопасны. - person keltar   schedule 20.03.2014alloca
? и почему, несмотря на такие проблемы, люди продолжали использовать его до такой степени, что он нашел свое место в стандарте? - person Shahbaz   schedule 20.03.2014n >= 0
можно решить, превративn
вsize_t
(в любом случае это то, что вы должны использовать для размеров). Если звонящий вводит отрицательное число, то последующие беспорядки ложатся на их ответственность. - person Kninnug   schedule 20.03.2014alloca
отлично подходит для небольших буферов, например. printf - лично я не хочу, чтобы он использовал malloc или другое распределение кучи. - person keltar   schedule 20.03.2014alloca
, не было бы причин для того, чтобы он стал стандартом (в форме VLA), поэтому должно быть достаточно людей, которые продолжали его использовать. Кроме того,alloca
отлично подходит для небольших буферов, что означает, что вы можете гарантировать, что буферы будут маленькими (если вы не можете, то они больше не являются маленькими буферами), т.е. вы можете гарантировать, что существует верхняя граница . Если вы можете это сделать, почему бы не получить массив фиксированного размера такого размера? Наконец, у меня есть реализацияfprintf
, и я не понимаю, почему вам может понадобиться VLA в ее реализации. - person Shahbaz   schedule 20.03.2014size_t
(стандарт используетint
в своих примерах, и я просто использовалint
, не задумываясь). Тем не менее,size_t
по-прежнему не защищает отn == 0
, поэтому вам все равно нужна проверка, будь тоif (n == 0) fail
илиif (n <= 0) fail
. - person Shahbaz   schedule 20.03.2014malloc
. Это было бы безумием. - person Shahbaz   schedule 20.03.2014fputc
записывает в буфер, аfprintf
используетfputc
. Что касается рекурсии, VLA не имеет смысла. Если ваш ввод большой, вы можете рекурсировать меньше, а если он маленький, вы можете больше рекурсировать? Требование часто наоборот. - person Shahbaz   schedule 20.03.2014n
, а затем перебрать элементыn
, еслиn = 0
это все еще не проблема. Да и вообще какой смысл сильно не аргументировать в пользу запрета чего-либо. - person harold   schedule 20.03.2014malloc
. Я имею в виду, что вы не выбираете между переполнением стека и segfault. Оба плохие. Вы можете обойтись и без того. - person Shahbaz   schedule 20.03.2014int array[N] = {0}
. Если стандарт требует, чтобыN
было строго положительным, все в порядке. Если он позволяетN
быть равным нулю, он должен затем создать угловой случай, говорящий, что в таком случае инициализация невозможна. Это было для массивов фиксированного размера (C11, 6.7.6.2-1). Вероятно, из-за симметрии они добавили такое же правило для VLA. - person Shahbaz   schedule 20.03.2014alloca()
и рекурсии верхняя граница глубины стека может быть определена еще до запуска программы. Таким образом, при вызове программы может быть назначено достаточное пространство стека. Рекурсия препятствует простому анализу необходимой глубины стека. Тем не менее рекурсия разрешена (во многих средах). ИМО, использование VLA принципиально не сопряжено с большим риском, чем рекурсия. Если коду не нужны риски VLA, запретите и рекурсию. - person chux - Reinstate Monica   schedule 20.03.2014