可変長配列がC ++標準に含まれていないのはなぜですか?


326

ここ数年はCをあまり使っていません。今日この質問を読んだとき、私は知らない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
好奇心から、なぜスタックに割り当てる必要があるのですか?あなたはヒープ割り当てのパフォーマンスの問題を恐れていますか?
Dimitri C.

32
@Dimitri実はそうではありませんが、スタックの割り当てがヒープの割り当てよりも速いことは否定できません。そして、場合によってはこれが問題になることがあります。
Andreas Brinck 09

11
可変長配列の主な利点は、すべてのデータが互いに接近しているため、この配列を反復処理するときに、バイトを隣同士で読み書きすることです。データはキャッシュにフェッチされ、CPUはメモリにバイトをフェッチしたり、メモリからバイトを送信したりせずに、キャッシュで作業できます。
カルマリウス

4
可変長配列は、プリプロセッサ定数を静的なconst変数に置き換えるためにも使用できます。また、CにはVLAの別のオプションがなく、ポータブルC / C ++コード(両方のコンパイラと互換性がある)を記述する必要がある場合があります。
Yury

2
余談ですが、clang ++ではVLAが許可されているようです。
user3426763 2014年

回答:


204

最近のUsenetで幕を開けたこのことについて議論がありました:なぜC ++ 0xの中にいないのVLA

スタック上に潜在的に大きな配列を作成する必要があり、通常は利用可能なスペースがほとんどないのはよくないという意見に同意します。引数は、サイズが事前にわかっている場合は、静的配列を使用できます。また、事前にサイズがわからない場合は、安全でないコードを記述します。

C99 VLAは、スペースを無駄にしたり、未使用の要素のコンストラクターを呼び出したりせずに小さな配列を作成できるという小さな利点を提供できますが、型システムにかなり大きな変更を導入します(ランタイム値に応じて型を指定できる必要があります-これnew演算子の型指定子を除いて、現在のC ++にはまだ存在しませんが、それらは特別に扱われるため、実行時性はnew演算子のスコープをエスケープしません。

を使用できますがstd::vector、動的メモリを使用するため、同じではありません。独自のスタックアロケータを使用するのは簡単ではありません(整列も問題です)。VLAは固定サイズであるのに対し、ベクターはサイズ変更可能なコンテナーであるため、同じ問題は解決しません。C ++動的配列の提案は、VLAベースの言語に代わるものとして、ライブラリベースのソリューションを紹介することを意図しています。ただし、私が知る限り、C ++ 0xの一部にはなりません。


22
+1して受け入れました。ただし、スタックオーバーフローを発生させる方法は他にもたくさんあるため、安全性についての議論は少し弱いと思います。安全引数は、再帰を決して使用してはならず、ヒープからすべてのオブジェクトを割り当てる必要があるという立場をサポートするために使用できます。
Andreas Brinck 09

17
スタックオーバーフローを発生させる方法は他にもあるので、もっと多くの方法を奨励するとしたら、
jalf

3
@Andreas、弱点について合意しました。しかし、再帰の場合、スタックが使い尽くされるまで膨大な数の呼び出しが必要です。それが事実である場合、人々は反復を使用します。ただし、usenetスレッドの一部の人が言うように、これはすべてのケースでVLAに対する議論ではありません。時々、あなたは間違いなく上限を知っている場合があるからです。しかし、それらの場合、静的配列はそれでもとにかく多くのスペースを無駄にしないので、静的配列でも同様に十分である可能性があります(もしそうなら、スタック領域が再び十分に大きいかどうかを実際に確認する必要があります)。
Johannes Schaub-litb 2009

10
また、そのスレッドでMatt Austernの回答を見てください。C++での型の一致がより厳密であるため、VLAの言語仕様はおそらくC ++ではかなり複雑になります(例:Cではa T(*)[]をに割り当てることができますT(*)[N]-C ++では、これは許可されていません。 「型の互換性」については知りません-完全一致が必要です)、型パラメーター、例外、コンストラクター、デストラクタなど。VLAの利点が実際にそのすべての成果を上げるかどうかはわかりません。しかし、私は実際にVLAを使用したことがないので、VLAの適切な使用例をおそらく知りません。
Johannes Schaub-litb 2009

1
@AHelps:おそらく、そのためにはいくぶんかのように振る舞うvectorが、固定のLIFO使用パターンを必要とし、静的に割り当てられた1つまたは複数のバッファーを維持するタイプが最適です。これまでに使用されましたが、明示的にトリミングできます。通常の「割り当て」では、一般的なケースでは、ポインタのコピー、ポインタからのポインタの減算、整数の比較、およびポインタの加算しか必要ありません。割り当てを解除するには、ポインタのコピーが必要です。VLAよりも遅くはありません。
スーパーキャット2015年

216

(背景:CおよびC ++コンパイラーの実装経験があります。)

C99の可変長配列は、基本的には失敗したものです。VLAをサポートするために、C99は次の譲歩を常識にしなければなりませんでした:

  • sizeof x常にコンパイル時定数ではなくなりました。コンパイラーはsizeof、実行時に-式を評価するコードを生成する必要がある場合があります。

  • 2次元VLA(int A[x][y])を許可するには、2D VLAをパラメーターとして取る関数を宣言するための新しい構文が必要でした:void foo(int n, int A[][*])

  • C ++の世界ではそれほど重要ではありませんが、組み込みシステムプログラマーのCの対象読者にとって非常に重要です。VLAを宣言することは、スタックの任意の大きなチャンクを選択することを意味します。これは保証されたスタックオーバーフローとクラッシュです。(あなたが宣言いつはint A[n]、あなたが「知っていれば、あなたは暗黙のうちにあなたがスペアに、スタックの2ギガバイトを持っていると主張している。結局、nここでは間違いなく1000未満である」、そしてあなただけ宣言しますint A[1000]。32ビット整数を代入nするためには1000入場ですあなたはあなたのプログラムの振る舞いがどうあるべきかを知らないということです。)

では、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<decltype(A1), decltype(A2)>()?その場合、への呼び出しはコンパイルするmyfunc べきではありませんテンプレートタイプの推定は失敗するため、であってはなりません!実行時にその動作をどのようにエミュレートできますか?

基本的に、C ++は、テンプレートコードの生成、関数の評価など、コンパイル時の決定をますますプッシュする方向に向かっconstexprています。その間、C99は従来のコンパイル時の決定(たとえばsizeof)をランタイムにプッシュするのに忙しかった。これを念頭に置いて、C99スタイルのVLAをC ++に統合しようとするあらゆる努力を費やすことは本当に意味がありますか?

他のすべての回答者がすでに指摘したように、C ++は、「必要なRAMの容量がわからない」という考えを本当に伝えたいときに、多くのヒープ割り当てメカニズム(std::unique_ptr<int[]> A = new int[n];またはstd::vector<int> A(n);明らかなメカニズム)を提供します。また、C ++は、必要なRAMの量がRAMの量よりも多いという避けられない状況に対処するための、優れた例外処理モデルを提供します。しかし、うまくいけば、この回答は、C99スタイルのVLAがC ++に適さなかった理由、およびC99に実際には適さなかった理由についての良い考えを提供します。;)


このトピックの詳細については、VLAに関するBjarne Stroustrupの2013年10月の論文、N3810「Alternatives for Array Extensions」を参照してください。BjarneのPOVは私のものとは非常に異なります。N3810は、物事に適したC ++風の構文を見つけることに重点を置き、C ++ で生の配列を使用しないようにすることに重点を置いていますが、メタプログラミングと型システムへの影響に重点を置いています。彼がメタプログラミング/タイプシステムの影響を解決、解決可能、または単に興味がないと見なしているかどうかはわかりません。


これらと同じ点の多くに当てはまる優れたブログ投稿は、「可変長配列の正当な使用」(Chris Wellons、2019-10-27)です。


15
VLAが間違っていたことに同意します。はるかに広く実装され、はるかに有用なものは、alloca()代わりにC99で標準化されているはずです。VLAは、標準委員会が逆にではなく実装の前に飛び出したときに起こることです。
MadScientist 2014年

10
可変的に変更された型システムはIMOに最適であり、箇条書きのいずれも常識に違反しません。(1)C標準は「コンパイル時」と「ランタイム」を区別しないため、これは問題ではありません。(2)*はオプションです。あなたは書くことができます(すべきです)int A[][n]。(3)VLAを実際に宣言せずに型システムを使用できます。たとえば、関数は可変的に変更された型の配列を受け入れることができ、異なる次元の非VLA 2D配列で呼び出すことができます。ただし、投稿の後半で有効なポイントを作成します。
MM

3
「VLAを宣言することは、スタックの任意の大きさのチャンクを選択することを意味します。これは保証されたスタックオーバーフローとクラッシュです。(int A [n]を宣言するときはいつでも、2GBのスタックに余裕があることを暗黙的に主張しています」 false。スタックオーバーフローなしで、2GBよりはるかに少ないスタックでVLAプログラムを実行したところ
Jeff

3
@ジェフ:nあなたのテストケースの最大値は何でしたか、そしてあなたのスタックのサイズは何でしたか?n少なくともスタックのサイズと同じ大きさの値を入力してみることをお勧めします。(そして、ユーザーがnプログラムのの値を制御する方法がない場合は、最大値をn直接宣言に伝搬することをお勧めします:宣言int A[1000]または必要なものは何でも。の最大値がn小さなコンパイル時定数によって制限されていない場合。)
Quuxplusone

2
alloca()はそのような組み込み関数を使用して実装できるため、コンパイラー標準関数としてalloca()を任意のプラットフォームに実装できることは、定義により真です。コンパイラがalloca()の最初のインスタンスを検出できず、コードに埋め込まれるマークとリリースのタイプを調整できなかった理由はありません。また、コンパイラが次の場合にヒープを使用してalloca()を実装できない理由はありません。スタックではできません。ハード/ポータブルでないの、Cコンパイラの上に alloca()が実装さているため、さまざまなコンパイラやオペレーティングシステムで動作します。
MadScientist 2017年

26

必要に応じて、常にalloca()を使用して、実行時にスタックにメモリを割り当てることができます。

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

スタックに割り当てられているということは、スタックがほどけるときに自動的に解放されることを意味します。

クイックノート:alloca(3)のMac OS Xマニュアルページで述べたように、「alloca()関数はマシンとコンパイラに依存します。その使用は推奨されません。」ちょうどあなたが知っているので。


4
また、alloca()のスコープは、変数を含むコードのブロックだけではなく、関数全体です。したがって、ループ内で使用すると、スタックが継続的に増加します。VLAにはこの問題はありません。
sashoalm

3
ただし、外側のブロックのスコープを持つVLAは、関数全体のスコープを持つalloca()よりもはるかに有用性が低いことを意味します。考慮:if (!p) { p = alloca(strlen(foo)+1); strcpy(p, foo); } これは、ブロックスコープのために、VLAでは実行できません。
MadScientist 2017年

1
それはOPのなぜ質問に答えません。さらに、これは-にC似たソリューションであり、実際には-とは言えませんC++
エイドリアンW

13

私自身の作業で、可変長の自動配列やalloca()などが必要になるたびに、メモリが物理的にCPUスタックに配置されていることを気にしていませんでした。一般的なヒープへの遅いトリップを引き起こさなかったいくつかのスタックアロケータ。だから私はそれが可変サイズのバッファをプッシュ/ポップできるメモリを所有するスレッドごとのオブジェクトを持っています。一部のプラットフォームでは、mmuを使用してこれを拡張できます。他のプラットフォームには固定サイズがあります(mmuがないため、通常は固定サイズのCPUスタックも伴います)。私が使用する1つのプラットフォーム(ハンドヘルドゲームコンソール)は、希少で高速なメモリに常駐しているため、とにかく貴重な小さなCPUスタックを持っています。

可変サイズのバッファーをCPUスタックにプッシュする必要が決してないということではありません。正直なところ、これが標準ではないことを発見したとき、そのコンセプトが言語に十分に適合しているように思われるので、私は驚いた。しかし、私にとっては、「可変サイズ」と「CPUスタックに物理的に配置する必要がある」という要件が一緒になったことはありません。それは速度についてだったので、私は独自の「データバッファの並列スタック」を作成しました。


12

実行される操作と比較して、ヒープメモリの割り当てに非常に負荷がかかる状況があります。例として、行列演算があります。小さめの行列を使用して5〜10個の要素を処理し、多くの演算を行う場合、mallocのオーバーヘッドは非常に大きくなります。同時に、サイズをコンパイル時定数にすることは、非常に無駄で柔軟性がないように見えます。

C ++自体は非常に安全ではないので、「安全でない機能を追加しないようにする」という主張はあまり強くありません。一方、C ++は間違いなく最もランタイム効率の高いプログラミング言語機能であるため、C ++は常に役立ちます。パフォーマンスが重要なプログラムを作成する人はC ++を大部分使用するため、できるだけ多くのパフォーマンスが必要です。ヒープからスタックにものを移動することは、そのような可能性の1つです。ヒープブロックの数を減らすことも別の方法です。VLAをオブジェクトメンバーとして許可することは、これを実現する1つの方法です。私はそのような提案に取り組んでいます。確かに、実装は少し複雑ですが、かなり実行可能のようです。


12

C ++ 14で利用できるようです:

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

更新:C ++ 14には含まれていません。


面白い。Herb Sutterが動的配列の下でここで議論しています:isocpp.org/blog/2013/04/trip-report-iso-c-spring-2013-meeting(これはウィキペディア情報のリファレンスです)
デフォルト

1
2014年1月18日にウィキペディアに78.86.152.103を書いた「実行時には、配列とDynArrayオブジェクトによっては、アレイの拡張技術仕様に移動されましたサイズの」:en.wikipedia.org/w/...
strager

10
ウィキペディアは規範的なリファレンスではありません:)この提案はC ++ 14に組み込まれませんでした。
MM

2
@ViktorSehr:このwrt C ++ 17のステータスは何ですか?
einpoklum 2016年

@einpoklumノーアイデア、利用ブースト::コンテナ:: static_vector
ヴィクトルたSehr

7

これはC ++ / 1xに含めると考えられていましたが、削除されました(これは、私が以前言ったことに対する修正です)。

いずれにせよ、C ++ではstd::vectorこの役割を既に満たす必要があるため、あまり役に立ちません。


42
いいえ、ありません。std:: vectorはスタックにデータを割り当てません。:)
コス

7
「スタック」は実装の詳細です。コンパイラは、オブジェクトの寿命に関する保証が満たされている限り、どこからでもメモリを割り当てることができます。
MM

1
@MM:結構ですが、実際std::vectorには、たとえばの代わりに使用することはできませんalloca()
einpoklum 2016年

@einpoklumは、プログラムの正しい出力を取得するという点では、可能です。パフォーマンスは実装品質の問題です
MM、

1
@MMの実装品質は移植できません。そして、パフォーマンスが必要ない場合は、そもそもc ++を使用しません
pal

3

これにはstd :: vectorを使用します。例えば:

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

メモリはヒープ上に割り当てられますが、これにはパフォーマンス上の小さな欠点しかありません。さらに、サイズに制限があるため、スタックに大きなデータブロックを割り当てないのが賢明です。


4
可変長配列の主な用途は、任意次数の多項式の評価です。その場合、「小さなパフォーマンス上の欠点」とは、「コードの実行が通常の場合の5倍遅くなる」ことを意味します。それは小さくありません。
AHelps '10 / 10/15

1
単純に使用しないのですstd::vector<int> values(n);か?resize構築後に使用することにより、移動できないタイプを禁止します。
LF

1

C99はVLAを許可します。また、VLAの宣言方法にもいくつかの制限があります。詳細は規格の6.7.5.2を参照してください。C ++はVLAを許可しません。しかし、g ++ではそれが可能です。


あなたが指している標準的な段落へのリンクを提供できますか?
Vincent、

0

このような配列はC99の一部ですが、標準C ++の一部ではありません。他の人が言ったように、ベクトルは常にはるかに優れたソリューションです。これが、可変サイズの配列がC ++標準(または提案されたC ++ 0x標準)にない理由です。

ところで、C ++標準が「なぜ」であるかについての質問については、モデレートされたUsenetニュースグループcomp.std.c ++にアクセスしてください。


6
-1ベクトルは常に良いとは限りません。多くの場合、そうです。常に、いいえ。小さな配列のみが必要で、ヒープスペースが遅いプラットフォーム上にあり、ライブラリのベクターの実装がヒープスペースを使用している場合、この機能が存在する場合は、この機能が非常に優れている可能性があります。
Patrick M

-1

コンパイル時に値がわかっている場合は、以下を実行できます。

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

}

編集:アロケーターはテンプレートパラメーターであるため、スタックアロケーター(alloca)を使用するベクトルを作成できます。


18
コンパイル時に値がわかっている場合は、テンプレートはまったく必要ありません。非テンプレート関数でXを直接使用するだけです。
ロブ・ケネディ

3
時々、呼び出し側はコンパイル時に認識し、呼び出し先は認識しません。それがテンプレートに適しています。もちろん、一般的なケースでは、実行時まで誰もXを知りません。
Qwertie 2012

STLアロケーターでallocaを使用することはできません-allocaから割り当てられたメモリは、スタックフレームが破棄されると解放されます。つまり、メモリを割り当てる必要のあるメソッドが戻ったときです。
オリバー

-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 2016年

あなたは単にboost :: container :: static_vectorを使うことができます。
Viktor Sehr 2016年

これには、MSVCより多くの生のアセンブリを持つ他のコンパイラに相当するものはありません。VCはesp変更を理解し、スタックへのアクセスを調整する可能性がありますが、たとえばGCCでは、少なくとも最適化を使用する場合-fomit-frame-pointerは特に、完全に中断します。
ルスラン、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.