std::vector
よりも機能を削って軽量・高速にした動的配列 std::dynarray
なるものが、過去のC++規格ドラフトにて提案されたことがあります。
いわば、std::arrray
の要素数をコンパイル時でなく実行時に指定できるようにしたものです。
C++14で追加されるという話は存在したものの結局は見送られ、今現在もなお使用できるようなる気配はありません。
以下にこの std::dynarray
の提案理由や技術周辺をざっと調べた結果をメモしておきます。踏み込んだ話題には乏しく、間違いも多々含まれる可能性があるためご了承ください。
概要 ―std::dynarray
とは―
C++を書いていて、実行時に初めて得られる値を長さとした、簡単な配列が欲しいときどうしましょう。色々思いつきますが、
-
malloc()
…C言語は半端に混ぜたくない -
new
…スマートポインタ等整備されてきたモダンC++では気が引ける -
std::make_unique<>()
…適しているが、配列ならば専用のクラスを使いたくなる -
std::array
…良い機能。しかしコンパイル時に長さが決まらないと使えない -
std::vector
…これが正解
。。。本当でしょうか?
確かにどんな場面でも、原則vector
を使えば間違いはないです。しかし、本当に短時間だけ確保できればよく、後々のサイズ変更も必要ないという状況もあります。例えば以下はOpenGLのエラーメッセージをchar(GLchar)の配列として取得するコードですが、こうしたC言語ベースのライブラリにおける文字列の扱いなどでは頻出のパターンかもしれません。
// 失敗していたらエラーログの例外を投げる
GLsizei buf_size; // ログメッセージの長さ
glGetShaderiv(shader_id, GL_INFO_LOG_LENGTH, &buf_size);
auto info_log = std::make_unique<GLchar[]>(buf_size); // ログの文字列を格納する領域
glGetShaderInfoLog(shader_id, buf_size, nullptr, info_log.get());
throw GLShaderCreationException(info_log.get()); // エラーログ付きのオリジナルの例外
文字数がbuf_size
なので、その長さのメモリ領域が必要です。ここではstd::make_unique<char[]>(buf_size)
とすることで、charの連続した領域をヒープに確保(&スマートポインタで管理)しています。
これはこれで全く問題なく動作するのですが、char[]
というある意味原始的な型を直接扱うのではなく、std::array
やatd::vector
といった配列専用のクラスを使えるならば使ったほうがより綺麗という考えもあります。(事実、上記のコードはclang-tidyによって Do not declare C-style arrays, use std::array<> instead (modernize-avoid-c-arrays) というヒントが発されます)
ヒントに愚直に従ってarray
を使おうにも、サイズがコンパイル時に決まらないためどうやっても不可能です。ならばと代わりにvector
を用いたくなりますが、文字列を受け取って例外へコピーするというだけのために、挿入や削除・サイズ変更に伴って自動でメモリ再確保を行うなどの高機能を備えるvector
を使うのは些かオーバースペックと感じます。1
そこで登場するのがarrayとvectorの中間に位置する、std::dynarray
です。
std::dynarray<int> foo(5); // 長さ5のデフォルト初期化された配列を確保
std::dynarray<int> bar(5, 3); // 初期値3、長さ5の配列を確保
のように使います。添字やイテレータによるアクセス・front()
, back()
, data()
などは可能ですが、vectorとは異なり要素数を変更するinsert()
やerase()
の操作はできません。付随してsizeとcapacityの相違などの概念も存在せず、メモリの再確保に関する機能が削減されていることが分かります。
また最も重要な点として、dynarrayはヒープでなくスタックにメモリを確保するという特長があります。
なんと可変長にもかかわらずヒープを使う必要が無いのです。一般論としてオーバーヘッドとなりうるヒープアロケーションを行わないという部分は、スマートポインタやvectorに対して明確に差別化できるポイントです。
スタック領域を利用した可変長技術
ここまで書いてきたものの、実は動的な固定長を持った配列の実現方法はdynarrayだけではありません。冒頭にも言及しておけばよかったかもしれませんね。
-
alloca()
関数 - GNU拡張もしくはC99以降で使用できるVLA
前者はズバリ、スタック領域にメモリを確保する関数です。後者のVLAはこちらなどで述べられているとおり、関数に制御が移りローカル変数を用意する際に、実行時に指定される回数だけスタックへのプッシュを繰り返すことで実現されているようです。
そもそもスタックに確保するもの(=ローカル変数)は固定サイズじゃないと駄目じゃないのかと初めは思うかもしれませんが、それはC/C++(Rustとかも)の言語仕様による制限であり、コンピュータの仕組み自体はもう少し柔軟です。現にアセンブリにはpushq
popq
命令があり、printfなどに代表されるC言語の可変長引数はこれらを駆使して実現されています。
その他雑記
「動的に定まる固定長」という話題からは少しずれますが、llvm::SmallVector
やboost::container::static_vector
& boost::container::small_vector
も存在するようです(別の方の解説記事)。用途によっては便利でしょう。
std::valarray
というのもあると後に知りました。しかしcharには使えないのかな?
-
実際の開発現場においては、ほとんどの場合
vector
による僅かな性能低下など無視すべきだと大前提として抑えておく必要はあります。「時期尚早な最適化は行わない」のを心に留めた上で、いざこれがボトルネックを生じる状況に遭遇した際、どうすればいいのかを考察することに意義があると思います。また、こうした知識に基づいて普段から意識をしていれば、全体的な性能の底上げに繋がる場合もあるでしょう。 ↩