【C++】配列とコンテナ

まえおき

10年ぶりくらいにC++を書いてみると、当たり前だけど分からないことだらけ。
これは、単に忘れていることもあるが、結構新しい機能が追加されたというのもある。私が知っているのは1998年の最初のバージョン。その後2011年に大きなバージョンアップがあり、14年、17年と改定。今後も20、23と予定されている。
とりあえずゆっくりかみしめるように勉強していきます。そんな備忘録。

配列

(※以下C言語的な素の配列を単に「配列」、std::vector等を「コンテナ」と呼びます)


C++11でstd::arrayという固定長配列のコンテナが追加され、(コンテナとしての)配列を使う必要性はほとんどなくなった?


std::beginとかstd::endとかはC++11から素の配列にも使え、いわゆるrange based forも配列にも使える。
そしてC++17で、std::sizeにより配列長も分かるようになった。(とはいえこれは以前からテンプレート関数を用いて配列長を取得するテクニックが公式化されたという感じ)

とはいえ、関数の引数でポインタで渡された配列なんかは、この新しいstd:sizeをもってしてもサイズを知ることはできない。


ということで、配列でも比較的安全に(今風に)書くことはある程度できそう。
使いどころによってはstd::arrayより見た目はすっきりしそう。
どっちを使うかっていうのはローカルなコーディングルールを作るんだろうなぁ。

…とりあえずこれからの自分的な方針としては、
できるだけstd::arrayで書いてみることにします。
そのうえで明らかに配列のほうがよさそうな場面を発見できるかどうか。

コード例

// C時代のやりかた
#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
// C++時代(C++17より前)のやりかた
template<typename T, std::size_t S> std::size_t array_size(const T(&)[S]) { return S; }

//----------------------------------------------

int a1[] = {0,1,2,3,4};

// C時代のsizeof
for (std::size_t i = 0; i < ARRAY_SIZE(a1); i++) { }
// C++時代のテンプレート関数
for (std::size_t i = 0; i < array_size(a1); i++) { }
// C++17のstd::size
for (std::size_t i = 0; i < std::size(a1); i++) { }

// 関数の引数の場合
ArrayFuncTest(a1);

//----------------------------------------------
// ArrayFuncTest の実装例
//----------------------------------------------
// これだとaはポインタになるのでテンプレート展開に失敗する
void ArrayFuncTest(int a[]) {  std::size_t s = std::size(a); }

// これに限った場合、配列サイズは5なのでこう書くとうまくいく
// (&a)と書かないといけないのはそうしないとポインタと認識されるため
void ArrayFuncTest(int (&a)[5]) {  std::size_t s = std::size(a); }

// ↑これを汎用的にするにはテンプレート関数にすればできますが、、、これは上記「array_size」と同じことで、今回やりたかったこととは少し違う。
template<std::size_t SIZE>
void ArrayFuncTest(int (&a)[SIZE]) { std::size_t s = SIZE; }

// じゃあこれは?
void ArrayFuncTest(int (&a)[]) {  std::size_t s = std::size(a); }
// 引数の型変換にも失敗するし、sizeのテンプレート展開にも失敗する
// error C2664: 'void ofApp::ArrayTestPrint(int (&)[])': 引数 1 を 'int [5]' から 'int (&)[]' へ変換できません。
// error C2672: 'std::size': 一致するオーバーロードされた関数が見つかりませんでした。
// error C2893: 関数テンプレート 'unknown-type std::size(const _Container &)' の特定に失敗しました


ちなみにサイズの型について、

// 昔の人はこう書きがちです。
for (int i = 0; i < std::size(a1); i++) { }
// sizeの戻り値はsize_tなのでint型のiとの比較でワーニングが出る。

// そんな時こそauto!?
for (auto i = 0; i < std::size(a1); i++) { }
// あれ、ダメなのか・・・この場合はautoはやっぱりintになる。

// こうしたらうまく推論できそうだけど・・。
for (auto i = 0, s = std::size(a1); i < s; i++) { }
// コンパイルエラー:
// error C3538: 宣言子リストの中で、'auto' の推論結果は常に同じ型でなければなりません。

残念です。


おまけ:噂のrange based forも配列に使えます。

// range based for
for (int n : a1) { }
// 便利やー。
// ただこれだと値をコピーしているのが気になるので、参照のほうがいいのかな。
for (int& n : a1) { }

参考

C++で静的配列の要素数を求めるテンプレート関数 | TECHSCORE BLOG
テンプレート関数による配列サイズの取得。
実は知らなかった。


生配列よりもstd::arrayを使った方が良い理由 - ぷろみん
配列とarrayの比較。ただこの記事の段階ではまだstd::sizeがない時代。


配列引数の要素数を調べる - meryngii.neta
配列の引数に(&)を付けないといけない部分の考察