Чому масиви змінної довжини не є частиною стандарту C ++?


326

Я не користувався С дуже багато останніх років. Прочитавши сьогодні це запитання, я натрапив на якийсь синтаксис 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++, зрозуміло, що це питання досить суперечливе з дуже важкими іменами з обох сторін аргументу. Звичайно, не очевидно, що a std::vector- завжди краще рішення.


3
Щойно з цікавості, чому його потрібно виділяти на стек? Ти не боїшся проблем з розподілом купи?
Димитрій К.

32
@Dimitri Насправді, але не можна заперечувати, що розподіл стеків буде швидше, ніж розподіл у купі. І в деяких випадках це може мати значення.
Андреас Брінк

11
Основна перевага масивів змінної довжини в тому, що всі дані близько один від одного, тому при повторенні через цей масив ви читаєте і записуєте байти поруч. Ваші дані забираються в кеш-пам'ять, а процесор може працювати над ним, не виймаючи і не надсилаючи байти в / з пам'яті.
Кальмарій

4
Масиви змінної довжини також можуть використовуватися для заміни препроцесорних констант статичними змінними const. Крім того, у C у вас немає інших варіантів для VLA, і іноді потрібно записати портативний код C / C ++ (сумісний з обома компіляторами).
Юрій

2
З іншого боку, здається, що кланг ++ дозволяє VLAs.
користувач3426763

Відповіді:


204

Нещодавно дискусія про це розпочалася у Usenet: Чому немає VLA в C ++ 0x .

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

CLA V99 можуть забезпечити невелику користь від можливості створювати невеликі масиви, не витрачаючи місця або викликаючи конструктори для невикористаних елементів, але вони внесуть досить великі зміни в систему типів (потрібно мати змогу вказувати типи залежно від значень часу виконання - це ще не існує в поточному C ++, за винятком newспецифікаторів типу оператора, але вони обробляються спеціально, так що час виконання не виходить із сфери дії newоператора).

Ви можете використовувати std::vector, але це не зовсім те саме, оскільки він використовує динамічну пам'ять, і змусити його використовувати власний розподільник стеків не зовсім простий (вирівнювання теж є проблемою). Це також не вирішує ту саму проблему, оскільки вектор є контейнером з можливістю зміни розміру, тоді як VLA мають фіксований розмір. Пропозиція C ++ Dynamic Array призначена для впровадження рішення, що базується на бібліотеці, як альтернатива мові на основі мови VLA. Однак, наскільки я знаю, це не буде частиною C ++ 0x.


22
+1 та прийнято. Хоча один коментар, я думаю, що аргумент безпеки трохи слабкий, оскільки існує так багато інших способів викликати переповнення стека. Аргумент безпеки може бути використаний для підтримки положення про те, що ніколи не слід використовувати рекурсію і що ви повинні виділяти всі об'єкти з купи.
Андреас Брінк

17
Отже, ви говорите, що, оскільки існують інші способи спричинити переповнення стека, ми можемо також заохотити більше їх?
jalf

3
@Andreas, погодився про слабкість. Але для рекурсії потрібно величезна кількість дзвінків, поки стек не з'їдеться, і якщо це може бути, люди використовуватимуть ітерацію. Як кажуть деякі люди з нитки Usenet, однак це не аргумент проти VLA у всіх випадках, оскільки іноді ви, напевно, можете знати верхню межу. Але в тих випадках, від того, що я бачу статичний масив може бути в рівній мірі достатньо, так як він не буде витрачати багато місця в будь-якому випадку (якщо це буде , то ви дійсно повинні запитати , є чи область стеки досить велика , знову ж ).
Йоханнес Шауб - ліб

10
Також подивіться на відповідь Метта Остерна в цій темі: Мовна специфікація VLAs, ймовірно, буде значно складнішою для C ++, оскільки суворіші відповідність типу в C ++ (приклад: C дозволяє призначити a T(*)[]до a T(*)[N]- у C ++ це заборонено, оскільки C ++ не знає про "сумісність типів" - для цього потрібні точні збіги), тип параметрів, винятки, кон- і деструктори та інше. Я не впевнений, чи справді вигоди від VLA дійсно окупляться цією роботою. Але тоді я ніколи не використовував VLA в реальному житті, тому я, мабуть, не знаю хороших випадків використання для них.
Йоханнес Шауб - ліб

1
@AHelps: Можливо, найкраще для цього був би тип, який поводиться дещо як, vectorале вимагає фіксованої схеми використання LIFO і підтримує один або кілька статично виділених буферів на один потік, які зазвичай розміром відповідно до найбільшого загального розподілу, що має нитка коли-небудь використовувався, але який можна було б чітко обрізати. Звичайне "виділення" в загальному випадку вимагає не більше ніж копія вказівника, віднімання вказівника від вказівника, порівняння цілих чисел та додавання покажчика; де-виділення просто вимагає копію вказівника. Не набагато повільніше, ніж VLA.
supercat

216

(Передумови: у мене є досвід впровадження компіляторів C і C ++.)

Масиви змінної довжини в C99 були в основному помилковим кроком. Щоб підтримати VLAs, C99 мусив піти на поступки здоровому глузду:

  • sizeof xбільше не завжди константа часу компіляції; компілятор іноді повинен генерувати код для оцінки sizeof-вираження під час виконання.

  • Дозвіл двомірного Власа ( int A[x][y]) потрібен новий синтаксис для оголошення функцій , які використовують 2D Влас в якості параметрів: void foo(int n, int A[][*]).

  • Менш важливо у світі C ++, але надзвичайно важливо для цільової аудиторії C програмістів із вбудованими системами, декларуючи VLA - це означає, що нарікаєш довільно великий шматок вашої стеки. Це гарантоване переповнення стека та збій. ( В будь-який час ви заявляєте int A[n], ви неявно стверджуючи , що у вас є 2 Гб стека запасних. В кінці кінців, якщо ви знаєте n«безумовно , менше , ніж 1000 тут», то ви б просто оголосити int A[1000]. Підставивши 32-розрядний ціле число nдля 1000є визнанням що ви не знаєте, якою має бути поведінка вашої програми.)

Гаразд, тепер перейдемо до розмови про C ++. У C ++ ми маємо такі ж сильні відмінності між "типовою системою" та "системою цінностей", що це робить C89 ... але ми справді почали покладатися на неї способами, якими цього немає. Наприклад:

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 повинен навіть компілюватися , оскільки виведення типу шаблона має бути невдалим! Як ми могли наслідувати цю поведінку під час виконання?

В основному, C ++ рухається в напрямку підштовхування все більшої кількості рішень у час компіляції : генерація шаблону коду, constexprоцінка функцій тощо. Тим часом, C99 був зайнятий підштовхуванням традиційних рішень для збирання часу (наприклад sizeof) у час виконання . Зважаючи на це, чи насправді навіть є сенс витрачати будь-які зусилля на намагання інтегрувати VLA в стилі C99 у C ++?

Як уже зазначав кожен інший відповідь, C ++ надає безліч механізмів розподілу купи ( std::unique_ptr<int[]> A = new int[n];або std::vector<int> A(n);явних), коли ви дійсно хочете донести ідею "я не маю уявлення, скільки оперативної пам'яті мені може знадобитися". А C ++ забезпечує чудову модель обробки винятків для вирішення неминучої ситуації, коли обсяг оперативної пам’яті, який вам потрібен, перевищує об’єм оперативної пам’яті. Але, сподіваємось, ця відповідь дає вам гарне уявлення про те, чому VLA-стилі у стилі C99 не дуже підходять для C ++ - і навіть не дуже добре підходять для C99. ;)


Детальніше з цієї теми див. N3810 "Альтернативи для розширення масиву" , документ Bjarne Stroustrup у жовтні 2013 року про VLAs. POV Bjarne сильно відрізняється від моєї; N3810 зосереджується більше на тому, щоб знайти хороший синтаксис C ++ для речей, а також на відмову від використання необроблених масивів у C ++, тоді як я більше зосередився на наслідках метапрограмування та типовій системі. Я не знаю, чи вважає він наслідки системи метапрограмування / типи вирішеними, вирішуваними чи просто нецікавими.


Гарна публікація в блозі, яка вражає багато цих самих моментів, - це "Законне використання масивів змінної довжини" (Chris Wellons, 2019-10-27).


15
Я погоджуюсь, що VLA просто помилялися. Набагато більш широко реалізовані та набагато корисніші alloca()повинні були бути стандартизовані замість C99. VLA - це те, що відбувається, коли комітет зі стандартів вистрибне перед реалізацією, а не навпаки.
MadScientist

10
Система змінного типу - чудове доповнення ІМО, і жодна з ваших точок кулі не порушує здоровий глузд. (1) стандарт C не розрізняє "час компіляції" та "час виконання", тому це не питання; (2) *необов’язковий, ви можете (і слід) писати int A[][n]; (3) Ви можете використовувати систему типу, не фактично декларуючи VLA. Наприклад, функція може приймати масив змінно модифікованого типу, і її можна викликати не-VLA 2-D масивами різних розмірів. Однак ви вказуєте дійсні бали в останній частині своєї публікації.
ММ

3
"оголосити VLA означає, що нарікає довільно великий шматок вашої стеки. Це гарантоване переповнення стека та збій. (Щоразу, коли ви заявите int A [n], ви неявно стверджуєте, що у вас є запас 2 Гб для запасного", це емпірично false. Щойно я запустив програму VLA зі стеком набагато менше 2 Гб без переповнення стека.
Джефф

3
@Jeff: Яке максимальне значення було nу вашому тестовому випадку та який розмір вашої стеки? Я пропоную спробувати ввести значення nпринаймні настільки ж великого, як розмір стека. (І якщо користувач не може контролювати значення nу вашій програмі, я пропоную вам просто поширити максимальне значення nпрямо в декларацію: int A[1000]заявіть або все, що вам потрібно. VLAs потрібні лише та небезпечні, коли максимальне значення nне обмежене якоюсь малою константою часу компіляції.)
Quuxplusone

2
Оскільки alloca () може бути реалізований за допомогою таких внутрішніх даних, це за визначенням вірно, що alloca () може бути реалізований на будь-якій платформі, як стандартна функція компілятора. Немає причини, що компілятор не міг виявити перший екземпляр alloca () та організувати вбудовані типи знаків та релізів у код, і немає причин, що компілятор не може реалізувати alloca () за допомогою купи, якщо це не можна зробити зі стеком. Важко / непереносимо - це аллока (), реалізована поверх компілятора С, щоб вона працювала в широкому наборі компіляторів та операційних систем.
MadScientist

26

Ви завжди можете використовувати alloca () для розподілу пам'яті на стеку під час виконання, якщо хочете:

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

Виділення на стек означає, що він автоматично буде звільнений, коли стек розкручується.

Швидке зауваження: Як зазначалося на макетній сторінці Mac OS X для аллока (3), "Функція аллока () залежить від машини та компілятора; її використання відключається". Просто щоб ти знав.


4
Також областю для alloca () є вся функція, а не лише блок коду, що містить змінну. Таким чином, використовуючи його всередині циклу, він буде постійно збільшувати стек. У VLA цієї проблеми немає.
sashoalm

3
Однак VLA, що мають область блоку, що вкладається, означає, що вони значно менш корисні, ніж аллока () з областю всієї функції. Поміркуйте: if (!p) { p = alloca(strlen(foo)+1); strcpy(p, foo); } цього неможливо зробити з VLA, саме через їх область блоку.
MadScientist

1
Це не відповідає чому питання ОП . Більше того, це подібне Cрішення, а не насправді - C++бажане рішення.
Адріан Ш

13

У своїй роботі я зрозумів, що кожного разу, коли мені хотілося чогось на зразок автоматичних масивів із змінною довжиною або аллока (), мені було зовсім не байдуже, щоб пам’ять фізично знаходилась на степі процесора, тільки що вона походить від деякий розподільник стеків, який не здійснював повільних поїздок до загальної купи. Отже, у мене є об'єкт на один потік, який володіє деякою пам'яттю, з якої він може натискати / висувати буфери змінного розміру. На деяких платформах я дозволяю цьому зростати через mmu. Інші платформи мають фіксований розмір (як правило, супроводжується стеком процесора фіксованого розміру, тому що немає mmu). Одна платформа, з якою я працюю (портативна ігрова консоль), так чи інакше має дорогоцінний стек процесора, оскільки він знаходиться в обмеженій, швидкій пам'яті.

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


12

Бувають ситуації, коли розподіл пам’яті купи досить дорого порівняно з виконаними операціями. Приклад - матрична математика. Якщо ви працюєте з дрібними матрицями, скажімо, від 5 до 10 елементів і виконайте чимало арифметики, накладні витрати будуть дуже важливими. У той же час зробити розмір постійним часом компіляції здається дуже марним і негнучким.

Я думаю, що C ++ настільки небезпечний сам по собі, що аргумент "намагатися не додавати більше небезпечних функцій" не дуже сильний. З іншого боку, оскільки C ++ - це, мабуть, найбільш ефективні функції мови програмування, що робить його більш корисним завжди: люди, які пишуть критичні програми для роботи, значною мірою використовують C ++, і їм потрібна максимальна продуктивність. Переміщення матеріалу з купи в стек - одна з таких можливостей. Зменшення кількості блоків купи - ще одне. Дозволити VLAs як учасників об'єкта був би одним із способів цього досягти. Я працюю над такою пропозицією. Це, правда, трохи складно реалізувати, але це здається цілком здійсненним.


12

Здається, він буде доступний на C ++ 14:

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

Оновлення: це не перетворилося на C ++ 14.


цікаво. Герб Саттер обговорює це тут під динамічними масивами : isocpp.org/blog/2013/04/trip-report-iso-c-spring-2013-meeting (це посилання на інформацію у Вікіпедії)
Типово

1
"Масиви та динарний масив, що виконується, переміщені до технічної специфікації розширення масиву", написано 18 січня 2014 року 78.86.152.103 у Вікіпедії: en.wikipedia.org/w/…
страгер

10
Вікіпедія не є нормативним посиланням :) Ця пропозиція не перетворила її на C ++ 14.
М.М.

2
@ViktorSehr: Який статус цієї Wrt C ++ 17?
einpoklum

@einpoklum Немає ідеї, використовуйте boost :: container :: static_vector
Viktor Sehr

7

Це було враховано для включення в C ++ / 1x, але було відхилено (це виправлення до того, що я говорив раніше).

У будь-якому випадку це буде менш корисно в C ++, оскільки нам вже std::vectorналежить виконати цю роль.


42
Ні, ми не робимо, std :: vector не виділяє дані в стеку. :)
Кос

7
"стек" - це деталь реалізації; компілятор може виділяти пам'ять з будь-якого місця, доки не будуть дотримані гарантії щодо терміну експлуатації об'єкта.
ММ

1
@MM: Ярмарок досить, але на практиці ми ще не можемо використовувати std::vectorзамість, скажімо, alloca().
einpoklum

@einpoklum з точки зору отримання правильного результату для вашої програми ви можете. Продуктивність - питання якості виконання
ММ

1
Якість якості впровадження @MM не є портативним. і якщо вам не потрібна продуктивність, ви не використовуєте с ++ в першу чергу
товариш

3

Використовуйте для цього std :: vector. Наприклад:

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

Пам'ять буде виділено на купі, але це має лише невеликий недолік продуктивності. Крім того, розумно не виділяти великі блоки даних на стеку, оскільки це досить обмежений розмір.


4
Основним застосуванням для масивів змінної довжини є оцінка поліномів довільних ступенів. У такому випадку ваш "малий недолік продуктивності" означає "код працює в п’ять разів повільніше в типових випадках". Це не мало.
AHelps

1
Чому ви просто не використовуєте std::vector<int> values(n);? Використовуючи resizeпісля побудови, ви забороняєте переміщувати типи.
LF

1

C99 дозволяє VLA. І це ставить деякі обмеження щодо того, як оголосити VLA. Детальніше дивіться в 6.7.5.2 стандарту. C ++ забороняє VLA. Але g ++ це дозволяє.


Чи можете ви надати посилання на стандартний абзац, який ви вказуєте?
Вінсент

0

Такі масиви є частиною C99, але не є частиною стандартних C ++. як говорили інші, вектор - це завжди набагато краще рішення, тому, ймовірно, масиви змінних розмірів відсутні у стандарті C ++ (або у запропонованому стандарті C ++ 0x).

До речі, для запитань щодо "чому" стандарт C ++ є таким, яким він є, модеровану групу новин Usenet comp.std.c ++ - це місце, куди потрібно звернутися.


6
-1 Вектор не завжди кращий. Часто так. Завжди, ні. Якщо вам потрібен лише невеликий масив, ви знаходитесь на платформі, де купа простору повільна, а реалізація вектора вашої бібліотеки використовує купі простір, то ця функція може бути краще, якби вона існувала.
Патрік М

-1

Якщо ви знаєте значення під час компіляції, ви можете зробити наступне:

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

}

Редагувати: Ви можете створити вектор, який використовує аллокатор стека (аллока), оскільки алокатор є параметром шаблону.


18
Якщо ви знаєте значення під час компіляції, шаблон вам зовсім не потрібен. Просто використовуйте X безпосередньо у функції, що не має шаблону.
Роб Кеннеді

3
Іноді абонент знає під час компіляції, а абонент цього не робить, саме для цього шаблони хороші. Звичайно, в загальному випадку ніхто не знає Х до часу запуску.
Qwertie

Ви не можете використовувати аллоку в алокаторі STL - виділена пам'ять з аллока буде звільнена при знищенні кадру стека - ось тоді повернеться метод, який повинен розподіляти пам'ять.
Олівер

-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?
einpoklum

Ви можете просто використовувати boost :: container :: static_vector ти.
Віктор Шер

Це не має еквівалентів для інших компіляторів, які мають більше необробленої збірки, ніж MSVC. VC, ймовірно, зрозуміє, що espзмінилося, і буде коригувати його доступ до стеку, але, наприклад, GCC ви просто розіб'єте його повністю - принаймні, якщо ви використовуєте оптимізацію, -fomit-frame-pointerзокрема.
Руслан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.