Почему массивы переменной длины не являются частью стандарта С++?

Я не использовал C очень много за последние несколько лет. Когда я прочитал этот вопрос сегодня, я наткнулся на некоторый синтаксис Си, с которым я не знаком.

По-видимому, в C99 допустим следующий синтаксис:

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

Это похоже на довольно полезную функцию. Было ли когда-либо обсуждение добавления его к стандарту С++, и если да, то почему он был опущен?

Некоторые потенциальные причины:

  • Волосатые для поставщиков компиляторов для реализации
  • Несовместим с какой-либо другой частью стандарта
  • Функциональность может быть эмулирована с помощью других конструкций С++

В стандарте С++ указывается, что размер массива должен быть постоянным выражением (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++, ясно, что этот вопрос довольно спорный с некоторыми очень тяжелыми именами по обе стороны аргумента. Совершенно очевидно, что a std::vector всегда является лучшим решением.

Ответы

Ответ 1

Недавно было обсуждено это в usenet: Почему нет VLA в С++ 0x.

Я согласен с теми людьми, которые, похоже, согласны с тем, что для создания потенциального большого массива в стеке, который обычно имеет только небольшое пространство, не очень хорошо. Аргумент состоит в том, что если вы заранее знаете размер, вы можете использовать статический массив. И если вы не знаете размер заранее, вы напишете небезопасный код.

C99 VLAs может обеспечить небольшое преимущество создания небольших массивов без потери пространства или вызова конструкторов для неиспользуемых элементов, но они вносят довольно большие изменения в систему типов (вы должны иметь возможность указывать типы в зависимости от времени выполнения values ​​- это еще не существует в текущем С++, за исключением спецификаторов типа new, но они обрабатываются специально, так что время выполнения не выходит за рамки оператора new).

Вы можете использовать std::vector, но это не совсем то же самое, что и использование динамической памяти, и использование одного собственного распределителя стека не совсем просто (выравнивание также является проблемой). Он также не решает одну и ту же проблему, поскольку вектор представляет собой изменяемый размер контейнера, тогда как VLA - фиксированный размер. Предложение С++ Dynamic Array предназначено для внедрения решения на базе библиотеки в качестве альтернативы языку VLA. Тем не менее, он не будет частью С++ 0x, насколько я знаю.

Ответ 2

(Предыстория: У меня есть опыт реализации компиляторов C и С++.)

Массивы переменной длины на C99 были в основном ошибочными. Чтобы поддерживать ОЛА, C99 должен был сделать следующие уступки здравому смыслу:

  • sizeof x больше не всегда является константой времени компиляции; компилятор должен иногда генерировать код для оценки выражения sizeof во время выполнения.

  • Разрешение двумерных VLA (int A[x][y]) потребовало нового синтаксиса для объявления функций, которые принимают 2D VLAs в качестве параметров: void foo(int n, int A[][*]).

  • Менее важно в мире С++, но чрезвычайно важно для целевой аудитории C программистов встроенных систем, объявляя VLA, это означает, что вы нажимаете произвольно большой кусок вашего стека. Это гарантированный переполнение стека и сбой. (В любое время, когда вы объявляете int A[n], вы неявно утверждаете, что у вас есть 2 ГБ стека. В конце концов, если вы знаете, что "n определенно меньше 1000 здесь", тогда вы просто объявите int A[1000]. 32-битное целое число n для 1000 является признанием того, что вы не знаете, каким должно быть поведение вашей программы.)

Хорошо, так что теперь переходим к разговору о С++. В С++ у нас такое же сильное различие между "системой типов" и "системой ценностей", что 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 не должен даже компилироваться, потому что вычет типа шаблона должен завершиться неудачно! Как мы могли бы эмулировать это поведение во время выполнения?

В основном, С++ движется в направлении выдвижения все большего количества решений во время компиляции: генерация кода шаблона, оценка функции constexpr и т.д. Между тем, C99 был занят толканием традиционно решений во время компиляции (например, sizeof) во время выполнения. Имея это в виду, действительно ли имеет смысл потратить все усилия на интеграцию VLA в стиле C99 в С++?

Как уже указывал любой другой ответчик, С++ предоставляет множество механизмов распределения кучи (std::unique_ptr<int[]> A = new int[n]; или std::vector<int> A(n);, являющихся очевидными), когда вы действительно хотите передать идею "Я не знаю, сколько ОЗУ я может понадобиться." И С++ предоставляет отличную модель обработки исключений для решения неизбежной ситуации, когда объем оперативной памяти вам больше, чем объем оперативной памяти. Но, надеюсь, этот ответ дает вам представление о том, почему VLA в стиле C99 не отлично подходит для С++ - и на самом деле даже не подходит для C99.;)


Подробнее об этой теме см. N3810 "Альтернативы для расширений массива" , документ Bjarne Stroustrup, октябрь 2013 года о VLA. Bjarne POV сильно отличается от моего; N3810 больше ориентируется на поиск хорошего синтаксиса Си ++ для этих вещей и на предотвращение использования необработанных массивов на С++, тогда как я больше сосредоточился на последствиях для метапрограммирования и системы типов. Я не знаю, считает ли он метапрограммирование/типы системными решениями решаемыми, разрешимыми или просто неинтересными.

Ответ 3

Вы всегда можете использовать alloca() для выделения памяти в стеке во время выполнения, если хотите:

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

Будучи выделенным в стеке, подразумевается, что он будет автоматически освобожден, когда пакет распанется.

Быстрое примечание. Как упоминалось в man-странице Mac OS X для alloca (3), "Функция alloca() зависит от машины и компилятора, ее использование не допускается". Просто чтобы вы знали.

Ответ 4

Бывают ситуации, когда выделение памяти кучи очень дорого по сравнению с выполненными операциями. Примером является математическая математика. Если вы работаете с маленькими матрицами, скажите от 5 до 10 элементов и делайте много арифметики, накладные расходы malloc будут действительно значительными. В то же время создание размера постоянной времени компиляции кажется очень расточительным и негибким.

Я думаю, что С++ настолько небезопасен сам по себе, что аргумент "попытаться не добавлять больше небезопасных функций" не очень силен. С другой стороны, поскольку С++, пожалуй, самые эффективные функциональные возможности языка программирования, которые делают его более полезным, всегда полезны: люди, которые пишут критически важные для производительности программы, в значительной степени будут использовать С++, и им нужно как можно больше производительности. Перемещение материала из кучи в стек - одна из таких возможностей. Уменьшение количества блоков кучи - другое. Разрешение VLA как членов объекта будет одним из способов достижения этого. Я работаю над таким предложением. По общему признанию, это немного сложно реализовать, но это кажется вполне выполнимым.

Ответ 5

В моей собственной работе я понял, что каждый раз, когда мне нужно что-то вроде автоматических массивов переменной длины или alloca(), мне все равно, что память физически находится в стеке процессора, он пришел из некоторого распределителя стека, который не вызвал медленных поездок в общую кучу. Таким образом, у меня есть объект per-thread, которому принадлежит некоторая память, из которой он может буферизовать/поп-переменные. На некоторых платформах я позволяю этому расти через mmu. Другие платформы имеют фиксированный размер (обычно они сопровождаются стеклом с фиксированным размером процессора, так как нет mmu). Одна платформа, с которой я работаю (портативная игровая консоль), имеет дорогостоящий маленький процессорный стоп, потому что он находится в скудной, быстрой памяти.

Я не говорю, что толкать буферы с переменным размером в стек процессора не требуется. Честно говоря, я был удивлен, когда обнаружил, что это не стандартно, так как это, конечно, похоже на то, что концепция достаточно хорошо вписывается в язык. Для меня, однако, требования "переменный размер" и "должны быть физически расположены в стеке процессора" никогда не возникали вместе. Это была скорость, поэтому я сделал свой собственный "параллельный стек для буферов данных".

Ответ 7

Это было рассмотрено для включения в С++/1x, но было удалено (это исправление к тому, что я сказал ранее).

Это было бы менее полезно в С++, так как у нас уже есть std::vector, чтобы заполнить эту роль.

Ответ 8

Используйте для этого std::vector. Например:

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

Память будет выделена в куче, но это имеет лишь небольшой недостаток производительности. Кроме того, разумно не выделять большие блоки данных в стеке, так как он довольно ограничен по размеру.

Ответ 9

C99 допускает VLA. И он устанавливает некоторые ограничения на то, как объявить VLA. Подробнее см. В 6.7.5.2 стандарта. С++ запрещает VLA. Но g++ позволяет это.

Ответ 10

Массивы, подобные этому, являются частью C99, но не являются частью стандартного С++. как говорят другие, вектор всегда является гораздо лучшим решением, и, вероятно, поэтому переменные массивы не находятся в С++ standatrd (или в предлагаемом стандарте С++ 0x).

Кстати, для вопросов о "почему" стандарт С++ такой, какой он есть, умеренная новостная группа Usenet comp.std.c++ - это место перейти к.

Ответ 11

Boost Multimimensional Array Libray обеспечивает гораздо более сильный синтаксис, чем std::vector, когда дело доходит до многомерных массивов.

Ответ 12

Если вы знаете значение во время компиляции, вы можете сделать следующее:

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

}

Изменить: вы можете создать вектор, который использует распределитель стека (alloca), поскольку распределитель является параметром шаблона.

Ответ 13

У меня есть решение, которое действительно сработало для меня. Я не хотел выделять память из-за фрагментации в рутине, которая должна выполняться много раз. Ответ чрезвычайно опасен, поэтому используйте его на свой страх и риск, но он использует сборку для резервирования места в стеке. В приведенном ниже примере используется массив символов (очевидно, для переменной другого размера потребуется больше памяти).

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. Спецификатор компилятора (может возникнуть проблема с перемещением между компиляторами). Я не пробовал, поэтому я действительно не знаю.

Ответ 14

Вам нужно постоянное выражение для объявления массива в C/С++.

Для массивов динамических размеров вам нужно выделить память в кучу, а затем управлять лифтом этой памяти.

void foo(int n) {
    int* values = new int[n]; //Declare a variable length array
    [...]
    delete [] values;
}