0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++の可変長配列、幻の `std::dynarray` に関するメモ

Posted at

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::arrayatd::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::SmallVectorboost::container::static_vector & boost::container::small_vectorも存在するようです(別の方の解説記事)。用途によっては便利でしょう。

std::valarrayというのもあると後に知りました。しかしcharには使えないのかな?

  1. 実際の開発現場においては、ほとんどの場合vectorによる僅かな性能低下など無視すべきだと大前提として抑えておく必要はあります。「時期尚早な最適化は行わない」のを心に留めた上で、いざこれがボトルネックを生じる状況に遭遇した際、どうすればいいのかを考察することに意義があると思います。また、こうした知識に基づいて普段から意識をしていれば、全体的な性能の底上げに繋がる場合もあるでしょう。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?