C++ には、配列っぽいものが何個かある。
主要っぽい4つの配列っぽいものを比較してみる。
生配列
まずはC言語の伝統を汲む普通の配列。
使い方はこんな感じ:
// clang++ -std=c++11 -Wall
#include <cstdio>
#include <iostream>
#include <iterator> // std::begin など
// 配列の参照で受けるときはこんな感じ。
template <size_t array_size>
void int_array_receiver(int const (&ary)[array_size]) {
for (auto i : ary) {
std::cout << i << " ";
}
std::cout << std::endl;
}
// C言語風に受けるときはこんな感じ。
void c_style(long const *p, size_t size) {
for (size_t i = 0; i < size; ++i) {
std::cout << p[i] << " ";
}
std::cout << std::endl;
}
// STL風に受けるときはこんな感じ。
void stl_style(char const *begin, char const * end) {
for( auto p = begin ; p != end ; ++p ){
std::cout << 0+*p << " ";
}
std::cout << std::endl;
}
void func() {
// 宣言と定義
int three_integers[] = {1, 2, 3}; // 要素数の指定は省略できる
long five_longs[5] = {9, 8, 7}; // { 9, 8, 7, 0, 0 } と同じ。
char hoge[] = "hoge"; // 文字列リテラルで初期化できる。null-terminator
// を含むので hogeは5要素
char fuga[7] = "fuga"; // 末尾の3つはゼロになる。
// 関数に渡す
// 配列の参照を渡すとサイズも渡せる
int_array_receiver(three_integers);
// C言語風に渡すときはサイズを計算する必要がある
c_style(five_longs, sizeof(five_longs) / sizeof(*five_longs));
// STL風に渡すときは std::begin などを使う
stl_style( std::begin(hoge), std::end(hoge) );
// range based for が使える
for (auto c : fuga) {
std::cout << c + 0 << " ";
}
std::cout << std::endl;
}
int main() { func(); }
C++11 から、std::begin
とか range based for が使えるようになって便利になった。
とはいえ、すぐにポインタになってしまうのは C言語からの伝統で変わっていない。
int foo[] = {1,2,3};
auto bar = foo;
のようにすると、bar
はポインタになってしまい、配列の中身はコピーされない。
また、サイズ0の配列を作ろうとするとコンパイルエラーになる。
std::array
C++11 で導入されたテンプレートクラス。C++11 以降が使えれば、生の配列よりもこちらを使うのがおすすめ。
使い方はこんな感じ:
// clang++ -std=c++11 -Wall
#include <array>
#include <cstdio>
#include <iostream>
#include <numeric>
template <typename ary_type> void ref_receive(ary_type const &ary) {
for (auto i : ary) {
std::cout << i << " ";
}
std::cout << std::endl;
}
void copy_receive(std::array<int, 3> ary) {
for (auto &i : ary) {
i += 10;
}
ref_receive(ary);
}
std::array<int, 4> returns_ary() { return {9, 8, 7, 6}; }
void func() {
// 宣言と定義
std::array<int, 100> garbage; // 初期化子リストがないと初期化されない
std::array<int, 3> three_integers = {1, 2, 3}; // 要素数の指定は省略できない
std::array<long, 5> five_longs = {9, 8, 7}; // { 9, 8, 7, 0, 0 } と同じ。
std::array<char, 5> hoge = {"hoge"}; // 文字列リテラルで初期化できる
std::array<char, 7> fuga = {"fuga"}; // 末尾の3つはゼロになる。
std::cout << std::accumulate(garbage.begin(), garbage.end(), 0) // ゴミが出る
<< std::endl;
// 複製
auto foo = three_integers; // コンストラクタで複製できる
decltype(three_integers) bar;
bar = foo; // 代入演算子でコピーできる
// 非参照で渡せばコピーされる( 関数内から bar を変更できない )
copy_receive(bar);
// 参照で渡せばコピーされない
ref_receive(bar);
// 関数の返戻値としても使える
auto baz = returns_ary();
ref_receive(baz);
}
int main() { func(); }
生配列がすぐに要素へのポインタに縮退してしまうのに対して、std::array
は普通の値のように振る舞うところが素晴らしい。
関数に渡すことも、関数から返してもらうこともできる。
STLの一員なので、begin()
, end()
, size()
などのメンバ関数が一通り揃っている。
生配列をリプレイスするものなので、動的な感じは全然ない。要素型はもちろん、サイズもコンパイル時に確定する。
生配列と異なり、要素数をゼロにしてもエラーにならない。
ただ。
要素数の省略ができない。生配列ならできるのに。残念。
std::unique_ptr<T[]>
unique_ptr じゃなくてもいいんだけど、例として unique_ptr で。
// clang++ -std=c++14 -Wall
#include <iostream>
#include <memory> // std::unique_ptr
std::unique_ptr<char[]> returns_ptr() {
auto p = std::make_unique<char[]>(10);
for (size_t ix = 0; ix < 10; ++ix) {
p[ix] = ix + 1 == 10 ? 0 : 'a' + ix;
}
return p;
}
void func(size_t n) {
// make_unique は C++14以降
// n は要素数。
// make_unique だとデフォルトコンストラクタが呼ばれる( int なら 0 になる)
auto p0 = std::make_unique<int[]>(n);
auto q = std::make_unique<int[]>(0); // サイズ0でもOK
// unique_ptr + new なら初期値を入れられる。要素数は省略不能。
auto p1 = std::unique_ptr<int[]>(new int[3]{11, 22, 33});
// char の配列を文字列で初期化。clang++ は OK だけど、g++-9 はエラー。
auto p2 = std::unique_ptr<char[]>(new char[5]{"hoge"});
std::cout << p2.get() << "\n";
// unique_ptr + new なら初期化を回避することもできる。
auto p3 = std::unique_ptr<int[]>(new int[3]);
for (size_t ix = 0; ix < 3; ++ix) { // range based for は使えない
std::cout << p3[ix] << " "; // 不定の値が出力される
}
std::cout << std::endl;
auto r = std::move(p0); // move できる
for (size_t ix = 0; ix < n; ++ix) { // range based for は使えない
std::cout << r[ix] << " ";
}
std::cout << std::endl;
auto s = returns_ptr(); // 返戻値として使える
std::cout << s.get() << std::endl;
}
int main() { func(3); }
ヒープにメモリ確保をする方法としては最も軽量だけど、できることが少ない。
特に、確保されている要素数を知る方法がないのが不便。
move ができるのでわりと使いやすい。
「int
を動的にn個確保したいけど 0で初期化する必要がない」とかいうときには malloc
するのがいいのかなぁ。
std::unique_ptr<int[]>(new int[3]);
の様にすることで、初期化を回避することができる。
C++20 からは std::std::make_unique_default_init
が使えるようだが、手元の clang++(Apple LLVM version 10.0.1 (clang-1001.0.46.4)), g++(9.1.0) で std=c++2a
にしても使えなかった。
上記のコードに書いたとおり、
new char[5]{"hoge"}
と書くと、clang++ はOKで、g++-9 はエラーにする。
どちらが正しいのかはまだ調べていない。謎。
vector
コードは省略。
だいたい何でもできる。
ただ、
std::vector<char> hoge = "hoge";
とか
std::vector<char> hoge{"hoge"};
は出来ない。残念。
中身は
- 先頭へのポインタ
- 有効な要素の末尾の次へのポインタ
- 確保されているメモリの末尾の次へのポインタ
の三点セットだと思うので、std::uniq_ptr<T[]>
よりはかさばる。大差ないけど。
あと。私の知る限り、int
等の 0 初期化を回避することはできない。
C++11 になってからは move できるので返戻値として抵抗なく使えるようになった。
まとめ
配列 | array |
unique_ptr<T[]> |
vector |
|
---|---|---|---|---|
要素数の省略 | ✅ | ❌ | ❌ | ✅ |
初期値の指定 | ✅ | ✅ | ✅※3 | ✅ |
文字列リテラルで初期化 | ✅ | ✅ | ? ※4 | ❌ |
サイズゼロ | ❌ | ✅ | ✅ | ✅ |
代入演算子で複製 | ❌ | ✅ | ❌ | ✅ |
サイズの指定 | 静的 | 静的 | 動的 | 動的 |
サイズ変更 | ❌ | ❌ | ❌ | ✅ |
range based for | ✅ | ✅ | ❌ | ✅ |
サイズの取得 | 計算※1 | o.size() |
できない | o.size() |
利用するメモリ | スタックとか | スタックとか | ヒープ※2 | ヒープ※2 |
move | ❌ | ❌※5 | ✅ | ✅ |
返戻値としての利用 | ❌ | ✅ | ✅ | ✅ |
0初期化 | 回避可能 | 回避可能 | 回避可能※3 | 不可避 |
※1: C++17 からは std::size(o)
が使える
※2: 頑張ればスタックにも作れると思う
※3: make_unique
ではなく、unique_ptr<型[]>(new 略);
とする必要がある。
※4: g++-9 と clang++ で意見が違う
※5: コメントいただいたとおり、出来なくはないけど意味があまりない。
上表のとおり、できることと出来ないことがいろいろある。
何を使うのが良いかはケースバイケース。