LoginSignup
19

More than 3 years have passed since last update.

生配列、array、unique_ptr<T[]>、vector

Last updated at Posted at 2019-04-21

C++ には、配列っぽいものが何個かある。
主要っぽい4つの配列っぽいものを比較してみる。

生配列

まずはC言語の伝統を汲む普通の配列。
使い方はこんな感じ:

C++11
// 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言語からの伝統で変わっていない。

C++11
int foo[] = {1,2,3};
auto bar = foo;

のようにすると、bar はポインタになってしまい、配列の中身はコピーされない。

また、サイズ0の配列を作ろうとするとコンパイルエラーになる。

std::array

C++11 で導入されたテンプレートクラス。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 で。

c++14
// 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: コメントいただいたとおり、出来なくはないけど意味があまりない。

上表のとおり、できることと出来ないことがいろいろある。
何を使うのが良いかはケースバイケース。

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
19