HOWTO · C++

C++で二重のstd::vectorを作成する方法

C++で行数と列数を指定した二重のstd::vectorを作成し、値を設定して各要素を変更する方法を紹介します。

このページの内容

「ベクトルのベクトル」は、各行を表す std::vector を外側の std::vector に格納した構造です。行数と列数が決まっている場合は、外側のベクトルに行数を指定し、各行となる内側のベクトルには列数と初期値を指定します。この記事では、サイズを指定したグリッドの作成、サンプル値による初期化、各要素の変更方法を順に説明します。行を個別のベクトルとして扱い、行数や列数をプログラムから決める場合に使える方法です。

サイズと初期値を指定するコンストラクタでC++の二重ベクトルを作成する

サイズと初期値を指定するコンストラクタなら、行数、列数、初期内容を明示できます。たとえば、std::vector<std::vector<int>> grid(rows, std::vector<int>(columns, 0)); は、columns 個の整数を持つ内側のベクトルを rows 個作り、各整数をゼロで初期化します。内側のベクトルは、外側の各行を初期化する値として構築されます。これはサイズと初期値を渡す方法で、デフォルトコンストラクタによる構築とは異なります。

次の例では、整数のグリッドを4行6列で作成します。ベクトルのインデックスは0から始まるため、[2][2] は3行目の3列目を指します。入れ子にした .at() の呼び出しは4行目の4列目を選びます。.at() は各インデックスを対応するベクトルのサイズと照合し、範囲外なら例外を送出します。インデックスが有効だと分かっている場合は、より簡潔な角括弧でアクセスできます。

#include <iostream>
#include <vector>

int main() {
  constexpr int LENGTH = 4;
  constexpr int WIDTH = 6;
  std::vector<std::vector<int>> grid(LENGTH, std::vector<int>(WIDTH, 0));

  grid[2][2] = 12;
  std::cout << grid[2][2] << '\n';

  grid.at(3).at(3) = 99;
  std::cout << grid[3][3] << '\n';
  return 0;
}
12
99

この出力は二つの代入で決まります。外側のベクトルで行を選び、その行の内側のベクトルで列を選びます。どちらのインデックスも0から始まります。次元が固定されていない場合は、プログラムのデータに応じて行数や各行のサイズを決めることもできます。

rand関数を使ってC++の二重ベクトルにサンプル値を設定する

グリッドを作成したら、入れ子のループで各整数を順に処理してサンプル値を代入できます。次のプログラムは現在時刻を使って rand の乱数系列を初期化し、% 100 で0から99までの値を得ます。値は実行ごとに変わるため、出力は一定ではありません。rand と srand には <cstdlib>、std::time には <ctime> が必要です。

#include <cstdlib>
#include <ctime>
#include <iostream>
#include <vector>

int main() {
  constexpr int LENGTH = 4;
  constexpr int WIDTH = 6;
  std::vector<std::vector<int>> grid(LENGTH, std::vector<int>(WIDTH, 0));

  std::srand(static_cast<unsigned int>(std::time(nullptr)));
  for (auto& row : grid) {
    for (int& value : row) {
      value = std::rand() % 100;
      std::cout << value << ' ';
    }
    std::cout << '\n';
  }
  return 0;
}

この方法は、デモや機密性を必要としない小規模な処理向けにサンプルデータを作るものです。rand は暗号用途には適していません。また、乱数生成器が取り得る値の個数が100で割り切れない場合、剰余演算によって各値の出やすさに偏りが生じます。シミュレーションや、より望ましい分布特性が必要なアプリケーションでは、この短い例を汎用の乱数生成器として使わず、要件に合った乱数機能を選んでください。

範囲ベースの入れ子ループでC++の二重ベクトルの各要素を変更する

範囲ベースのループで各行をたどり、内側のループでその行の整数を一つずつ処理できます。int& value の & は、value が格納済みの要素への参照であることを示します。そのため、value に代入するとベクトル内の値が変わります。参照を付けない場合、ループ変数は要素のコピーになり、変更しても元の整数には反映されません。次のプログラムは、各要素に3を掛ける前と後のグリッドを表示します。

#include <iostream>
#include <vector>

void print(const std::vector<std::vector<int>>& grid) {
  for (const auto& row : grid) {
    for (int value : row) {
      std::cout << value << ' ';
    }
    std::cout << '\n';
  }
}

int main() {
  std::vector<std::vector<int>> grid{{1, 2, 3}, {4, 5, 6}};
  std::cout << "Before:\n";
  print(grid);

  for (auto& row : grid) {
    for (int& value : row) {
      value *= 3;
    }
  }

  std::cout << "After:\n";
  print(grid);
  return 0;
}

各内側のベクトルは整数の並びを個別に管理し、外側のベクトルは行となるベクトルを格納します。同じ行の整数は連続して格納されますが、入れ子全体が一つの連続した二次元領域になるわけではありません。これは格納方法の説明で、一般的な速度の優劣を意味するものではありません。性能は処理内容やワークロードによって異なります。行を個別に扱う場合や通常のベクトル操作を使う場合には、この構造が便利です。一つの連続した領域が必要なら、その配置に適したデータ表現を選んでください。