C++のstd::vector::sizeについて

AI実装検定のご案内

目次

std::vector::size()とは

std::vector::size()は、vectorに現在格納されている要素数を取得するためのメンバ関数です。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> numbers{10, 20, 30, 40};

    std::cout << numbers.size() << '\n';
}

実行結果は次のとおりです。

4

numbersには4個の要素が格納されているため、size()4を返します。

size()が返すのは、使用メモリ量やバイト数ではありません。

あくまで、vector内に存在している要素の個数です。

std::vector::size()の基本仕様

std::vector::size()は、概念的には次のようなメンバ関数です。

constexpr size_type size() const noexcept;

主な特徴は次のとおりです。

  • 引数を取らない
  • vectorの要素数を返す
  • vectorの内容を変更しない
  • 空のvectorでは0を返す
  • 例外を送出しない
  • 計算量は定数時間
  • 戻り値の型はsize_type

constexprとして利用できるのは、C++20以降を前提とした環境です。

通常の実行時コードでは、C++のバージョンを特に意識せず使用できます。

size()の戻り値型

size()の戻り値は、intではなく次の型です。

std::vector<T>::size_type

size_typeは、vectorの要素数を表現するための符号なし整数型です。

一般的な標準ライブラリの実装では、std::size_tと同じ型として定義されています。

ただし、C++標準上は、必ずstd::size_tと同じ型でなければならないと断定するより、十分な範囲を持つ符号なし整数型と理解する方が正確です。

型を明示する場合は、次のように書けます。

std::vector<int> values{1, 2, 3};

std::vector<int>::size_type count = values.size();

一般的には、autoを使うと簡潔です。

const auto count = values.size();

autoを使えば、size()の戻り値型をそのまま受け取れるため、型の不一致を避けやすくなります。

size()は要素数を返す

size()が返すのは、vector内に存在する要素の個数です。

std::vector<int> values{10, 20, 30};

std::cout << values.size() << '\n';

出力結果は次のとおりです。

3

仮にintが1個4バイトの環境であっても、size()が返す値は12ではなく3です。

要素部分のバイト数を計算する方法

格納されている要素部分の単純なバイト数を求める場合は、次のように計算できます。

const auto bytes = values.size() * sizeof(int);

要素型を直接記述したくない場合は、次のようにも書けます。

const auto bytes = values.size() * sizeof(values[0]);

sizeofの中にある式は通常評価されないため、vectorが空であっても、実際にvalues[0]へアクセスする処理は発生しません。

ただし、この計算で求められるのは、存在している要素部分の単純なサイズだけです。

次のような領域は含まれません。

  • vectorオブジェクト自身の管理情報
  • capacity()で確保されている未使用領域
  • メモリアロケータの管理領域
  • メモリアライメントによる余分な領域

そのため、vectorが実際に使用している総メモリ量を正確に表すものではありません。

空のvectorとsize()

要素が1つも格納されていないvectorでは、size()0を返します。

std::vector<int> values;

std::cout << values.size() << '\n';

出力結果は次のとおりです。

0

空判定にはempty()を使う

vectorが空かどうかは、size()でも確認できます。

if (values.size() == 0) {
    std::cout << "空です\n";
}

ただし、空判定には通常、empty()を使用します。

if (values.empty()) {
    std::cout << "空です\n";
}

どちらも正しく動作しますが、empty()の方が「空かどうかを調べている」という意図が明確です。

反対に、要素が存在するかどうかを確認する場合は、次のように書けます。

if (!values.empty()) {
    std::cout << "要素があります\n";
}

size()とcapacity()の違い

size()capacity()は、意味が異なります。

size()

size()は、現在実際に存在している要素数を返します。

std::vector<int> values{10, 20, 30};

std::cout << values.size() << '\n';

出力結果は3です。

capacity()

capacity()は、再度メモリを確保し直さずに保持できる要素数を返します。

std::vector<int> values;

values.reserve(100);

std::cout << "size: " << values.size() << '\n';
std::cout << "capacity: " << values.capacity() << '\n';

reserve(100)を実行しても、要素は追加されません。

そのため、次の関係になります。

values.size() == 0
values.capacity() >= 100

多くの実装ではcapacity()がちょうど100になりますが、C++標準が保証しているのは、少なくとも100以上になることです。

size()とcapacity()の関係

capacity()は、常にsize()以上です。

values.size() <= values.capacity()

存在しているすべての要素を保持する必要があるため、capacity()size()より小さくなることはありません。

reserve()とsize()の関係

reserve()は、将来要素を追加するための領域をあらかじめ確保する関数です。

std::vector<int> values;

values.reserve(10);

この時点では、要素は存在していません。

std::cout << values.size() << '\n';

出力結果は0です。

reserve()後に添字アクセスしてはいけない

次のコードは正しくありません。

std::vector<int> values;

values.reserve(10);
values[0] = 100;

reserve(10)によって記憶領域は確保されますが、intの要素はまだ作られていません。

そのため、values[0]へのアクセスは未定義動作です。

要素を追加する場合は、push_back()を使用します。

values.push_back(100);

または、先にresize()で要素を作成します。

values.resize(10);
values[0] = 100;

reserve()が再確保を抑える仕組み

vectorに要素を追加し続けると、確保済み領域が不足した時点で、より大きな領域を確保し直すことがあります。

values.push_back(10);

再確保が発生すると、既存の要素が新しい領域へ移動されます。

事前にreserve()を使用すると、要素数がcapacity()を超えるまでは、追加処理による再確保を避けられます。

std::vector<int> values;
values.reserve(100);

for (int i = 0; i < 100; ++i) {
    values.push_back(i);
}

この例では、100個までの要素追加に必要な領域を先に確保しています。

resize()とsize()の関係

resize()は、vectorの要素数そのものを変更する関数です。

std::vector<int> values;

values.resize(5);

std::cout << values.size() << '\n';

出力結果は次のとおりです。

5

resize(5)を実行すると、5個の要素が実際に存在する状態になります。

resize()で要素数を増やす場合

std::vector<int>の要素数を増やした場合、追加されたint要素は0になります。

std::vector<int> values;

values.resize(5);

for (int value : values) {
    std::cout << value << ' ';
}

出力例は次のとおりです。

0 0 0 0 0

ただし、クラス型を格納している場合は、その型に対応する方法で要素が生成されます。

std::vector<MyClass> objects;
objects.resize(5);

この場合、追加された要素はMyClassのデフォルト構築に相当する処理で生成されます。

resize()で要素数を減らす場合

現在の要素数より小さい値を指定すると、末尾の要素が削除されます。

std::vector<int> values{10, 20, 30, 40, 50};

values.resize(3);

std::cout << values.size() << '\n';

出力結果は3です。

残る要素は次の3個です。

10 20 30

reserve()とresize()の違い

reserve()resize()は名前が似ていますが、役割が異なります。

std::vector<int> a;
a.reserve(10);

std::vector<int> b;
b.resize(10);

結果は次のようになります。

vectorsize()状態
a0領域だけを確保している
b1010個の要素が存在している

reserve()

a.reserve(10);

reserve()には次の特徴があります。

  • size()は変わらない
  • 要素は作成されない
  • capacity()が指定値以上になる
  • 添字で要素へアクセスできるようにはならない

resize()

b.resize(10);

resize()には次の特徴があります。

  • size()が指定した値になる
  • 実際に要素が作成される
  • b[0]からb[9]までアクセスできる

要素の追加とsize()

push_back()emplace_back()で要素を追加すると、size()は増加します。

std::vector<int> values;

std::cout << values.size() << '\n';

values.push_back(10);
std::cout << values.size() << '\n';

values.emplace_back(20);
std::cout << values.size() << '\n';

出力結果は次のとおりです。

0
1
2

要素を1個追加するたびに、size()も1ずつ増えます。

要素の削除とsize()

要素を削除すると、size()は減少します。

pop_back()

pop_back()は末尾の要素を1個削除します。

std::vector<int> values{10, 20, 30};

values.pop_back();

std::cout << values.size() << '\n';

出力結果は2です。

空のvectorに対してpop_back()を呼び出してはいけません。

if (!values.empty()) {
    values.pop_back();
}

erase()

erase()は、指定した位置の要素を削除します。

std::vector<int> values{10, 20, 30};

values.erase(values.begin());

std::cout << values.size() << '\n';

出力結果は2です。

clear()

clear()は、すべての要素を削除します。

std::vector<int> values{10, 20, 30};

values.clear();

std::cout << values.size() << '\n';

出力結果は0です。

ただし、clear()を実行しても、確保済みの容量は通常そのまま残ります。

std::cout << values.capacity() << '\n';

clear()は要素を削除する処理であり、確保済みメモリを必ず解放する処理ではありません。

size()を使ったループ

インデックスを使用してvectorを走査する場合は、次のように書けます。

std::vector<int> values{10, 20, 30};

for (std::size_t i = 0; i < values.size(); ++i) {
    std::cout << values[i] << '\n';
}

intを使う場合の注意

次のように書くと、コンパイラによっては警告が出ます。

for (int i = 0; i < values.size(); ++i) {
    std::cout << values[i] << '\n';
}

理由は、比較している型が異なるためです。

  • iは符号付き整数型
  • values.size()は符号なし整数型

警告を避けるには、std::size_tやvectorのsize_typeを使用します。

for (std::size_t i = 0; i < values.size(); ++i) {
    std::cout << values[i] << '\n';
}

または、次のように書けます。

for (std::vector<int>::size_type i = 0;
     i < values.size();
     ++i) {
    std::cout << values[i] << '\n';
}

インデックスが不要なら範囲ベースfor文を使う

単純にすべての要素を処理するだけであれば、範囲ベースfor文の方が簡潔です。

for (const int value : values) {
    std::cout << value << '\n';
}

要素を変更する場合は、参照を使用します。

for (int& value : values) {
    value *= 2;
}

大きなクラスオブジェクトをコピーせずに読み取る場合は、const参照を使用します。

for (const auto& value : values) {
    std::cout << value << '\n';
}

std::ssize()を使う場合

C++20以降では、std::ssize()を使って、コンテナの要素数を符号付き整数型として取得できます。

#include <iterator>
#include <vector>

std::vector<int> values{10, 20, 30};

auto count = std::ssize(values);

std::ssize()の戻り値は、必ずintになるわけではありません。

十分な範囲を持つ符号付き整数型になります。

そのため、次のようにintへ固定すると、非常に大きなvectorでは範囲が不足する可能性があります。

int count = std::ssize(values);

通常の前方向ループでは、無理にstd::ssize()を使用せず、次の書き方で十分です。

for (std::size_t i = 0; i < values.size(); ++i) {
    std::cout << values[i] << '\n';
}

std::ssize()は、符号付き整数として差分計算や逆方向処理を行いたい場合に便利です。

有効なインデックスとsize()

vectorの要素数がNの場合、有効なインデックスは次の範囲です。

0 ~ N - 1

例えば、次のvectorには3個の要素があります。

std::vector<int> values{10, 20, 30};
values.size() == 3

有効なアクセスは次のとおりです。

values[0]
values[1]
values[2]

次のアクセスは範囲外です。

values[values.size()]

そのため、ループ条件は<=ではなく<にします。

正しい書き方は次のとおりです。

for (std::size_t i = 0; i < values.size(); ++i) {
    std::cout << values[i] << '\n';
}

次の書き方は誤りです。

for (std::size_t i = 0; i <= values.size(); ++i) {
    std::cout << values[i] << '\n';
}

最後の反復で、values[values.size()]へアクセスしてしまいます。

size() – 1を使う場合の注意

最後の要素のインデックスは、vectorが空でなければ次のように表せます。

values.size() - 1

ただし、空のvectorでは問題が発生します。

std::vector<int> values;

auto last_index = values.size() - 1;

size()の戻り値は符号なし整数型です。

そのため、0 - 1は数学的な-1ではなく、非常に大きな符号なし整数値へ回り込みます。

最後の要素へアクセスする場合は、先に空でないことを確認します。

if (!values.empty()) {
    std::cout << values[values.size() - 1] << '\n';
}

最後の要素を取得したいだけであれば、back()の方が明確です。

if (!values.empty()) {
    std::cout << values.back() << '\n';
}

back()も空のvectorに対して呼び出してはいけません。

逆順ループとsize()

次の逆順ループは正しくありません。

for (std::size_t i = values.size() - 1; i >= 0; --i) {
    std::cout << values[i] << '\n';
}

std::size_tは符号なし整数型なので、i >= 0は常に真になります。

また、vectorが空の場合は、最初のvalues.size() - 1の時点で回り込みが発生します。

符号なし整数に対応した書き方

次のように書けば、空のvectorにも対応できます。

for (std::size_t i = values.size(); i-- > 0;) {
    std::cout << values[i] << '\n';
}

ただし、この書き方は慣れていないと読みづらい場合があります。

逆イテレータを使う

逆順処理では、逆イテレータを使う方が意図を理解しやすくなります。

for (auto it = values.rbegin(); it != values.rend(); ++it) {
    std::cout << *it << '\n';
}

C++20のviews::reverseを使う

C++20以降では、次のようにも書けます。

#include <ranges>

for (const auto& value : values | std::views::reverse) {
    std::cout << value << '\n';
}

インデックスが不要な逆順処理では、逆イテレータやstd::views::reverseを使用する方が安全です。

operator[]とat()の違い

vectorの要素へ添字でアクセスする方法には、operator[]at()があります。

operator[]

std::cout << values[1] << '\n';

operator[]は範囲チェックを要求しません。

そのため、範囲外のインデックスを指定すると未定義動作になります。

std::cout << values[values.size()] << '\n';

at()

at()は、指定されたインデックスが範囲内かどうかを確認します。

std::cout << values.at(1) << '\n';

範囲外の場合は、std::out_of_range例外を送出します。

#include <stdexcept>

try {
    std::cout << values.at(values.size()) << '\n';
}
catch (const std::out_of_range& e) {
    std::cerr << e.what() << '\n';
}

外部入力などからインデックスが決まる場合は、at()を使うことで範囲外アクセスを検出しやすくなります。

size()の計算量

std::vector::size()の計算量は、定数時間です。

O(1)

要素が10個でも100万個でも、先頭から要素を数え直す処理ではありません。

一般的なvectorの実装では、先頭位置や末尾位置などの管理情報から、要素数をすぐに取得できます。

そのため、次のコードで毎回size()を呼んでも、毎回vector全体を走査するわけではありません。

for (std::size_t i = 0; i < values.size(); ++i) {
    std::cout << values[i] << '\n';
}

size()を変数へ保存する意味

次のように、ループ前に要素数を保存することもできます。

const auto count = values.size();

for (std::size_t i = 0; i < count; ++i) {
    std::cout << values[i] << '\n';
}

これは、必ずしも高速化のために必要な処理ではありません。

主な意味は、ループ開始時点の要素数を固定することです。

ループ中に要素を追加する場合

次のコードでは、ループ中にsize()が増え続けます。

std::vector<int> values{1, 2, 3};

for (std::size_t i = 0; i < values.size(); ++i) {
    values.push_back(values[i] * 10);
}

1回処理するたびに要素が追加されるため、終了条件も先へ延び続けます。

その結果、ループが終了せず、最終的にメモリ確保に失敗する可能性があります。

ループ開始時点の要素だけを処理する場合は、元の要素数を保存します。

std::vector<int> values{1, 2, 3};

const auto original_size = values.size();

for (std::size_t i = 0; i < original_size; ++i) {
    values.push_back(values[i] * 10);
}

処理後の内容は次のようになります。

1 2 3 10 20 30

push_back()による参照の無効化

push_back()によって再確保が発生すると、既存要素を指しているポインタ、参照、イテレータが無効になることがあります。

std::vector<int> values{10, 20, 30};

int& reference = values[0];

values.push_back(40);

// 再確保が発生していた場合、referenceは無効

ループ中に要素を追加する場合は、参照やイテレータの無効化にも注意が必要です。

constなvectorとsize()

size()はvectorの内容を変更しないため、constなvectorでも呼び出せます。

const std::vector<int> values{10, 20, 30};

std::cout << values.size() << '\n';

関数の引数をconst参照で受け取った場合でも使用できます。

void print_size(const std::vector<int>& values)
{
    std::cout << values.size() << '\n';
}

vectorのサイズを調べるだけであれば、値渡しではなくconst参照を使用する方が適切です。

void print_size(std::vector<int> values)
{
    std::cout << values.size() << '\n';
}

この書き方では、関数呼び出し時にvector全体がコピーされる可能性があります。

読み取りだけなら、次のようにします。

void print_size(const std::vector<int>& values)
{
    std::cout << values.size() << '\n';
}

size()とmax_size()の違い

size()max_size()も意味が異なります。

size()

size()は現在の要素数を返します。

values.size()

max_size()

max_size()は、実装上vectorが保持できる理論的な最大要素数を返します。

values.max_size()

例えば、次のように確認できます。

std::vector<int> values;

std::cout << "現在の要素数: "
          << values.size() << '\n';

std::cout << "理論上の最大要素数: "
          << values.max_size() << '\n';

max_size()は、実際にその個数までメモリを確保できることを保証するものではありません。

実際に確保できる要素数は、次のような条件に左右されます。

  • 利用可能なメモリ
  • アドレス空間
  • OSの制限
  • メモリ断片化
  • 使用しているアロケータ

二次元vectorとsize()

二次元vectorでは、外側と内側でsize()の意味が異なります。

std::vector<std::vector<int>> matrix{
    {1, 2, 3},
    {4, 5, 6}
};

外側のsize()は、行数を表します。

std::cout << matrix.size() << '\n';

出力結果は2です。

各行のsize()は、その行に含まれる要素数を表します。

std::cout << matrix[0].size() << '\n';

出力結果は3です。

各行の長さは同じとは限らない

次のように、行ごとに要素数が異なるvectorも作成できます。

std::vector<std::vector<int>> data{
    {1, 2},
    {3, 4, 5},
    {6}
};

各サイズは次のとおりです。

data.size()     // 3
data[0].size()  // 2
data[1].size()  // 3
data[2].size()  // 1

安全に走査する場合は、各行のsize()を使用します。

for (std::size_t row = 0; row < data.size(); ++row) {
    for (std::size_t column = 0;
         column < data[row].size();
         ++column) {
        std::cout << data[row][column] << ' ';
    }

    std::cout << '\n';
}

インデックスが不要なら、範囲ベースfor文の方が簡潔です。

for (const auto& row : data) {
    for (const auto& value : row) {
        std::cout << value << ' ';
    }

    std::cout << '\n';
}

vectorとsize()

std::vector<bool>は特殊化されたvectorですが、size()の意味は通常のvectorと同じです。

std::vector<bool> flags{true, false, true};

std::cout << flags.size() << '\n';

出力結果は3です。

内部的にビット単位で格納される実装であっても、size()が返すのは論理的なbool要素の個数です。

組み込み配列とsize()

組み込み配列には、メンバ関数としてのsize()はありません。

int values[]{1, 2, 3};

// values.size();  // 使用できない

C++17以降では、std::size()を使用できます。

#include <iterator>

int values[]{1, 2, 3};

std::cout << std::size(values) << '\n';

std::size()はvectorにも使用できます。

std::vector<int> values{1, 2, 3};

std::cout << std::size(values) << '\n';

配列と標準コンテナを似た書き方で扱いたい場合に便利です。

constexprでのsize()

C++20以降に対応した環境では、条件を満たす場合、vectorのsize()を定数評価で利用できます。

#include <vector>

constexpr bool check()
{
    std::vector<int> values{1, 2, 3};

    return values.size() == 3;
}

static_assert(check());

この例では、コンパイル時にvalues.size() == 3であることが確認されます。

コンパイラと標準ライブラリがC++20以降のconstexpr std::vectorに対応している必要があります。

マルチスレッドでの注意

size()はvectorを変更しない関数ですが、別のスレッドが同じvectorを同時に変更している場合は安全とは限りません。

例えば、一方のスレッドが次を実行しているとします。

values.push_back(10);

同時に別のスレッドが、同期せずに次を実行すると、データ競合になる可能性があります。

auto count = values.size();

同じvectorに対して読み取りと書き込みを複数スレッドで行う場合は、std::mutexなどによる同期が必要です。

#include <mutex>
#include <vector>

std::vector<int> values;
std::mutex mutex;

std::size_t get_size()
{
    std::lock_guard<std::mutex> lock(mutex);

    return values.size();
}

size()constメンバ関数であることと、別スレッドからの同時変更に対して安全であることは別の問題です。

std::vector::size()でよくある間違い

capacity()を要素数だと思う

std::vector<int> values;

values.reserve(100);

std::cout << values.size() << '\n';
std::cout << values.capacity() << '\n';

size()0です。

capacity()100以上になります。

確保済み領域と、実際に存在する要素数は異なります。

size()番目の要素へアクセスする

values[values.size()]

これは範囲外アクセスです。

最後の要素へアクセスする場合は、空でないことを確認してback()を使用します。

if (!values.empty()) {
    std::cout << values.back() << '\n';
}

空のvectorでsize() – 1を計算する

std::size_t index = values.size() - 1;

空のvectorでは、符号なし整数の回り込みが発生します。

if (!values.empty()) {
    const auto index = values.size() - 1;
}

reserve()後に添字アクセスする

values.reserve(10);
values[0] = 123;

reserve()では要素は作成されないため、未定義動作です。

正しくは次のようにします。

values.resize(10);
values[0] = 123;

または、要素を追加します。

values.push_back(123);

符号なし型でi >= 0を使う

for (std::size_t i = values.size() - 1; i >= 0; --i) {
}

std::size_tは符号なし型なので、i >= 0は常に真になります。

逆順処理には逆イテレータを使用すると安全です。

for (auto it = values.rbegin(); it != values.rend(); ++it) {
    std::cout << *it << '\n';
}

std::vector::size()の実用例

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> scores{85, 92, 78, 96};

    std::cout << "要素数: "
              << scores.size() << '\n';

    if (!scores.empty()) {
        std::cout << "最初の要素: "
                  << scores.front() << '\n';

        std::cout << "最後の要素: "
                  << scores.back() << '\n';
    }

    for (std::size_t i = 0;
         i < scores.size();
         ++i) {
        std::cout << i
                  << "番目: "
                  << scores[i]
                  << '\n';
    }

    scores.push_back(88);

    std::cout << "追加後の要素数: "
              << scores.size()
              << '\n';

    scores.clear();

    std::cout << "clear後の要素数: "
              << scores.size()
              << '\n';
}

実行結果の例は次のとおりです。

要素数: 4
最初の要素: 85
最後の要素: 96
0番目: 85
1番目: 92
2番目: 78
3番目: 96
追加後の要素数: 5
clear後の要素数: 0

std::vector::size()の要点

std::vector::size()を使用する際は、次の点を押さえておくことが重要です。

基本的なポイント

  • 現在存在する要素数を返す
  • 使用メモリ量やバイト数は返さない
  • 空のvectorでは0を返す
  • 戻り値はsize_type
  • size_typeは符号なし整数型
  • 計算量は定数時間

capacity()やreserve()との違い

  • capacity()は再確保なしで保持できる要素数
  • reserve()は領域だけを確保する
  • reserve()ではsize()は増えない
  • resize()は実際の要素数を変更する

添字アクセスの注意点

  • 有効なインデックスは0からsize() - 1
  • values[values.size()]は範囲外
  • 空のvectorでsize() - 1を計算してはいけない
  • 最後の要素には、空判定後にback()を使う

ループでの注意点

  • 符号付き整数とsize()を比較すると警告が出ることがある
  • インデックスにはstd::size_tsize_typeを使用する
  • インデックスが不要なら範囲ベースfor文を使用する
  • 符号なし整数による逆順ループでは終了条件に注意する
  • ループ中に要素を追加すると、size()が変化する

基本的には、次の3つの書き方を覚えておくとよいでしょう。

const auto count = values.size();
if (!values.empty()) {
    std::cout << values.back() << '\n';
}
for (const auto& value : values) {
    std::cout << value << '\n';
}

以上、C++のstd::vector::sizeについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次