Modern C++編【標準ライブラリ】 第6章 vector

先頭へ戻る

この章の概要

この章の概要です。


概要

std::vector は、動的に要素数を変更できる配列を表現したクラステンプレートです
メンバ変数として、動的な配列を保持しており、配列操作のために便利なメンバ関数を多数備えています。解放は、デストラクタなどが適切なタイミングで自動的に行ってくれます。

std::vector を使うには、<vector> という名前の標準ヘッダをインクルードする必要があります。

動的に配列を確保する必要がある場面では、std::vector を使うことを真剣に検討して下さい。new[] と delete[] を使って、独自で動的配列を管理する方法は、特に解放周りで慎重なプログラミングが必要で、無意味に危険です。std::vector を使った方が安全です。

std::vector を正しく使うためには、「サイズ」と「容量」の概念をしっかりと理解する必要があります。これは、この後の項で解説します

通常の配列と比べて欠点があるとすれば、メモリをやや多めに確保する場合があるということです。これは、要素を追加するたびに、メモリ領域を確保するよりも、ある程度一括で確保した方が、一般的に効率が良いからです。ですから一概に悪い訳ではないですが、実際に必要になる要素数がごく少ない場合には、想定よりもずっと多くのメモリが消費されるかも知れないことに注意が必要です。

実際に確保されているメモリ領域の量(何要素分か)は、capacityメンバ関数で調べられます。

std::vector は、次のように定義されています。

namespace std {
    template <typename T, typename Allocator = allocator<T>>
    class vector {
    };
}

std::vector はクラステンプレートなので、使用時にはテンプレート実引数の指定が必要です。テンプレート仮引数 T は、動的配列の要素の型です。例えば、int型の要素を扱いたいのであれば、以下のようになります。

std::vector<int> intVec;

もう1つのテンプレート仮引数 Allocator は、メモリ確保の方法を定義するアロケータと呼ばれるオブジェクトの指定です。テンプレート仮引数 Allocator は、デフォルトテンプレート実引数(【言語解説】第28章)を持っているので省略できます。
多くの場合、省略しても std::vector は活用できるので、本章では解説しません。アロケータについては、第25章で改めて取り上げます。


サイズと容量

std::vector を正しく使うためにはまず、「サイズ」と「容量」という概念を理解して下さい。

std::vector は、内部に動的な生配列を保持しています。つまり、以下のようなメンバ変数を持っているということです。

template <typename T, typename Allocator = allocator<T>>
class vector {
private:
    T*  mData;  // 動的な生配列。実際に確保されている要素数が「容量」
};

標準ライブラリ全般に言えることですが、当サイトで示す内部実装コードは例に過ぎないことに注意して下さい。標準規格の文面にあるルールに沿っていれば、具体的な実装方法は、ライブラリ提供者に任されていますから、当サイトで示すコードとはまったく違う方法を使っている可能性があります(当サイトのコードは、説明用にかなり単純化しています)。

この動的配列は、必要に応じて自動的に拡張されます。このとき、どのような方法でメモリを確保するのかを、アロケータ(2つ目のテンプレート仮引数)で指定できます。これを指定しないことは、標準の方法で構わないという意味になります(特別に事情が無い限り、これで問題ありません)。

実際に確保されている動的配列の要素数のことを「容量(キャパシティ)」と呼びます。容量は、capacityメンバ関数で取得できます。

size_type capacity() const noexcept;

noexcept は例外を送出しないことを表します。【言語解説】第18章を参照して下さい。

戻り値は、std::vector::size_type型です。これは、符号無しの整数型の typedef です。
容量は、バイト数のような単位ではなく、「要素数」であることに注意して下さい。実際に使用しているメモリ量は「sizeof(T) * capacity()」で求める必要があります。

一方「サイズ」とは、動的配列内に実際に存在している(使用されている)要素数のことです。サイズは、sizeメンバ関数で取得できます。

size_type size() const noexcept;

「サイズ」も「容量」と同様、バイト数ではなく要素数です。

容量とサイズの違いが分かっても、あまりピンと来ない人が多いかもしれません。理解しづらく感じる人は、「確保した領域のすべてが使われている状態」をイメージしてしまっているのかも知れません。こういうイメージだと、「容量」も「サイズ」も同じなのではないかと考えるでしょう。

実際には例えば、32要素分のメモリ領域が確保されているが(つまり「容量」が 32)、まだ要素が1つも存在していない(つまり「サイズ」が 0)という状態があります。要素を「追加(挿入)」して初めて「サイズ」が増加していきます。いわば「枠は作ったけれど、中身はまだ埋まっていない」という状態がある(現実的には、ほとんどいつもそういう状態になります)ということです。

生成と初期化

std::vector には、複数のコンストラクタが定義されているので、様々な方法でインスタンス化できます。

#include <vector>

int main()
{
    const int a[] = {0, 1, 2, 3, 4};

    std::vector<int> v1;                 // 空
    std::vector<int> v2(10);             // サイズ10 (要素はデフォルトコンストラクタで生成)
    std::vector<int> v3(10, 3);          // サイズ10 (要素はコピーコンストラクタで生成)
    std::vector<int> v4(v2);             // 他の vector からコピー
    std::vector<int> v5(a, a + 5);       // イテレータで指定された範囲からコピー
    std::vector<int> v6 {0, 1, 2};       // std::initializer_list
    std::vector<int> v7(std::move(v6));  // ムーブコンストラクタ
}

v2 以外の使い方では、それぞれの引数の末尾にアロケータを指定できます。これはデフォルト実引数があるので、標準のアロケータで構わなければ、省略できます。

C++14 で、v2 の形でもアロケータが指定できるようになっています。

v5 のところのコメントに書いてあるイテレータについては、「イテレータ」の項で取り上げます。このタイプのコンストラクタを使うと、既存の配列の要素を std::vector に取り込めます。

v6 の std::initializer_list については、第8章を参照して下さい。

v2 の方法を使った場合、引数で指定している数が「サイズ」であることに注意して下さい。指定した数だけ、オブジェクトが実際に生成されているので、それなりに処理時間が掛かります。もし、vector内部の動的配列を事前に確保したいという目的なのであれば、「サイズ」ではなく「容量」を指定すべきですから、この方法を使うのではなく、reserveメンバ関数を使うのが適切です。

std::vector<MyClass> v(10);   // サイズが 10 (要素を作るため、10回のコンストラクタ呼び出しがある)

std::vector<MyClass> v;
v.reserve(10);            // 容量が 10(以上) (要素は作られないので、コンストラクタの呼び出しは無い)

C++14 (生成と初期化)

サイズを指定するコンストラクタに、アロケータを指定できるようになりました。他のコンストラクタと同様、デフォルト実引数があり、指定を省略した場合は、標準のアロケータが使用されます。

破棄

デストラクタでは、std::vector が内部で確保した動的配列が解放されます。

仮想デストラクタ(【言語解説】第36章)ではないので、 std::vector の派生クラスを定義して使用することは、不適切な場合があるので注意して下さい

メンバ型

std::vector には、幾つかの型名が「公開」されたメンバとして定義されています。以下、表にまとめて紹介しますが、メンバ型の名前と、その意味だけがポイントとなります。具体的にどう定義されているかは、実装によって異なります。

まず、要素の型に関する定義があります。

メンバ型名 意味
value_type 要素の型。テンプレート仮引数 T のこと。
reference 要素の参照型。T&
const_reference 要素の const参照型。const T&
pointer 要素のポインタ型。T*
const_pointer 要素の constポインタ型。const T*

大きさや距離に関する型があります。

メンバ型名 意味
size_type 主に要素数を意味する型。符号無し整数。
多くの実装で std::size_t と同じ。
difference_type 主に距離を意味する型。符号付き整数。
多くの実装で std::ptrdiff_t と同じ。

イテレータに関する型があります。イテレータについては、「イテレータ」の項で説明しています。

メンバ型名 意味
iterator イテレータ型
const_iterator constイテレータ型
reverse_iterator 逆イテレータ型
const_reverse_iterator const逆イテレータ型

アロケータに関する型があります。アロケータについては、第25章で説明します。

メンバ型名 意味
allocator_type アロケータ型

要素のアクセス

[]演算子

std::vector は、普通の配列のように [] を使った添字アクセスが可能です。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v(5);

    v[3] = 10;
    std::vector<int>::value_type a = v[3];

    std::cout << a << std::endl;
}

実行結果:

10

添字に 0未満の数や、現在の「サイズ」以上の数を指定した場合、範囲外アクセスになってしまいます。これは普通の配列の場合と同じで、何が起こるか分からない危険な操作です。

std::vector は、要素を挿入するときに、容量が不足していたら、自動的にメモリの再確保を行いますが、[] はその対象外であることにも注意が必要です。v[3] = 10 のような操作は、要素の挿入ではなく、既存の要素の値を上書きする行為なのです。
有効な添字の上限値が「容量 - 1」ではなく「サイズ - 1」であることの意味は、この考え方から分かるでしょう。指定したその位置に、要素がすでに存在していなければならない訳です。

at

範囲外アクセスで未定義の動作になってしまうことを防ぎ、備えが欲しければ、atメンバ関数を使用します。atメンバ関数は、添字を引数に取り、その位置にある要素の参照を返します。[] を使う場合と違うのは、範囲外アクセスになったときに std::out_of_range例外を送出する点です

例外については、【言語解説】第18章で、std::out_of_range については第12章で解説しますが、ここでは使用例だけ挙げておきます。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v(10);

    try {
        v.at(3) = 10;
        int a = v.at(3);
        std::cout << a << std::endl;

        v.at(50) = 10;  // 範囲外アクセス

        // 実行されない
        std::cout << "!!!!!" << std::endl;
    }
    catch (const std::out_of_range& ex) {
        std::cerr << ex.what() << std::endl;
    }
}

実行結果:

10
invalid vector<T> subscript

実行結果の2行目は、VisualStudio の場合のものです。この部分は、環境によって異なるはずです。

front、back

また、std::vector の先頭要素(の参照)を frontメンバ関数で、末尾の要素(の参照)を backメンバ関数で取得できます。これらのメンバ関数は、std::vector が空の場合には未定義の動作になることに注意して下さい

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v(10);

    const std::vector<int>::size_type size = v.size();
    for (int i = 0; i < static_cast<int>(size); ++i) {
        v[i] = i;
    }

    std::cout << v.front() << std::endl;
    std::cout << v.back() << std::endl;
}

実行結果:

0
9

data

vector内部で管理されている生配列の先頭メモリアドレスを返す dataメンバ関数もあります。

#include <iostream>
#include <vector>

namespace {
    void PrintArray(const int* array, std::size_t size)
    {
        for (std::size_t i = 0; i < size; ++i) {
            std::cout << array[i] << std::endl;
        }
    }
}

int main()
{
    std::vector<int> v(5);

    const std::vector<int>::size_type size = v.size();
    for (int i = 0; i < static_cast<int>(size); ++i) {
        v[i] = i;
    }

    PrintArray(v.data(), v.size());
}

実行結果:

0
1
2
3
4

std::vector内部にある配列は、その要素がメモリ上に並んでいることが保証されているので、このサンプルプログラムのように、普通の配列の先頭メモリアドレスが返されたと考えて使用できます。通常の配列を使った旧式の API との連携が取りやすくなりました。

なお、dataメンバ関数が返したメモリアドレスを p とすると、p ~ p[v.size()-1] の範囲しか安全なアクセスは保証されません。また、vector が空の場合の結果は未定義です

代入

代入演算子

std::vector は、テンプレート実引数が同一であれば、代入演算子を使ってコピー、あるいはムーブができます。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    IntVector v(5);

    const IntVector::size_type size = v.size();
    for (int i = 0; i < static_cast<int>(size); ++i) {
        v[i] = i;
    }

    IntVector v2;
    v2 = v;  // v のコピー
    PrintVector(v);

    v2 = { 0, 1, 2 };  // 0, 1, 2 の 3つを代入
    PrintVector(v);
}

実行結果:

0
1
2
3
4

0
1
2
3
4

また、ムーブ代入演算子を使うこともできます。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    IntVector v1(5, 3);
    IntVector v2;

    v2 = std::move(v1);
    PrintVector(v1);
    PrintVector(v2);
}

実行結果:


3
3
3
3
3

assign

std::vector は、テンプレート実引数が同一であれば、assignメンバ関数を使うこともできます。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    IntVector v;

    v.assign(5, 3);  // 5個の 3 を代入
    PrintVector(v);

    const int a[10] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};
    v.assign(a, a + 10);  // 範囲内の要素を代入
    PrintVector(v);

    v.assign({ 0, 10, 20 });  // 0, 10, 20 の 3つを代入
    PrintVector(v);
}

実行結果:

3
3
3
3
3

0
10
20
30
40
50
60
70
80
90

0
10
20

assignメンバ関数の1つ目の使い方は、第2引数に指定した要素のコピーを、第1引数に指定した個数だけ代入します。
2つ目の方は、イテレータを使って範囲を指定し、その範囲内にある要素のコピーを代入します。
3つ目は、std::initializer_list(第8章)を使ったものです。

追加・挿入

要素を追加する処理に共通する注意事項としては、「容量」が足りないときには、動的配列の再確保が行われるという点が挙げられます。

再確保は、単純に重たい処理です。これは「新しい領域を確保する」「以前の領域にあった要素を、新しい領域へコピーする」「古い領域を解放する」という処理が行われるためです。

なお、新たな容量がどれだけになるかは、ライブラリの実装次第であり、明確に定められてはいません。例えば、元の容量の2倍を確保するかもしれませんし、1.5倍かもしれません。もっと複雑な実装がなされている可能性もあります。

もう1つの問題として、再確保が行われることによって、各要素のメモリアドレスが変化し得るという点があります。要素が新しい領域へ移し替えられるため、各要素のメモリアドレスは変わってしまうのが普通です。
そのため、要素を指すポインタや参照、イテレータは、要素の追加処理の直後には正しい位置を指していない可能性があるので、改めて取得し直す必要があります。

実装によっては、動的配列の後方に使われていないメモリ領域があれば、これを使うように再確保が行われるかもしれません。その場合、再確保の前後でメモリアドレスは変化しませんが、これを期待したコードを書いてはいけません。

push_back

std::vector へ要素を追加する際、使用頻度が高いのが push_backメンバ関数です。

push_backメンバ関数は、std::vector が管理する動的配列の末尾にある未使用部分へ、引数に指定した要素を追加します。もし、未使用部分が無く追加できないときは、動的配列を再確保します。
コピーを追加する方法と、ムーブを使って追加する方法があります。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    IntVector v;

    for (int i = 0; i < 5; ++i) {
        v.push_back(i);
    }

    PrintVector(v);
}

実行結果:

0
1
2
3
4

insert

要素を任意の位置へ挿入するには、insertメンバ関数を使います。この関数はオーバーロードされていて、使い方が幾つかあります。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    IntVector v(3, 5);             // 3個の 5

    IntVector::iterator it;

    it = v.insert(v.end(), 0);     // 末尾に 0 を挿入
    PrintVector(v);
    std::cout << "-----" << std::endl;

    v.insert(it, 3, 99);           // 0 の手前に 99 を 3個挿入
    PrintVector(v);
    std::cout << "-----" << std::endl;

    const int a[] = {10, 11, 12};
    v.insert(v.begin(), a, a + 3); // 指定範囲から先頭へ挿入
    PrintVector(v);
    std::cout << "-----" << std::endl;
    v.insert(v.begin(), {-1, -2, -3}); // 初期化リストから挿入

    PrintVector(v);
}

実行結果:

5
5
5
0

-----
5
5
5
99
99
99
0

-----
10
11
12
5
5
5
99
99
99
0

-----
-1
-2
-3
10
11
12
5
5
5
99
99
99
0

使い方が4通りありますが、どれでも第1引数が挿入位置を表しています。挿入位置は、イテレータを使って指定し、そのイテレータが指している位置へ挿入されます。例えば、「v.begin()」で取得したイテレータを指定すれば、先頭要素になるように挿入できます。

1つ目の使い方では、第2引数に挿入する値を指定します。この形式の場合は、戻り値で、挿入された値を指すイテレータが返されます。この形式では、ムーブによる挿入が可能です。
2つ目の使い方では、同じ値を複数個まとめて挿入できます。第2引数が個数、第3引数が挿入する値です。
3つ目の使い方では、第2第3の引数で指定した範囲内にある要素を挿入します。これは、別のコンテナや配列からコピーしたい場合に使います。
4つ目の使い方では、第2引数に std::initializer_list(第8章)を使います。

push_backメンバ関数と同様、「容量」が不足した場合には再確保が行われます。

std::vector の要素は動的配列で管理されているため、割り込み地の後続の要素を、1つずつずらす必要があります。そのため、途中に割り込む形で要素を挿入するのは、コストが掛かることに注意して下さい。

emplace_back

push_backメンバ関数は、追加したい要素を std::vector の外で作り、それをコピー(またはムーブ)する必要があります。この流れでは少なくとも、一時オブジェクトを生成するコストが掛かるため(コピーの場合は、更にコピーのコスト)、非効率さがあります。

emplace_backメンバ関数は、要素のコンストラクタに渡す実引数を指定するようにして、メンバ関数内でオブジェクトを作らせるという方法で、要素を追加できます。

#include <iostream>
#include <vector>

struct MyStruct {
    MyStruct(int a, const char* b) : mA(a), mB(b)
    {}

    int mA;
    const char* mB;
};

using MyStructVector = std::vector<MyStruct>;

int main()
{
    MyStructVector v;
    v.emplace_back(10, "abc");  // MyStruct のコンストラクタの実引数を指定

    const MyStructVector::value_type& s = v.back();

    std::cout << s.mA << ", " << s.mB << std::endl;
}

実行結果:

10, abc

「容量」が不足していれば自動的に拡張されます。

emplace

push_backメンバ関数に対して emplace_backメンバ関数があるように、insertメンバ関数に対応する emplaceメンバ関数があります。

emplace_backメンバ関数と同様、メンバ関数内でオブジェクトを作らせるという方法で、要素を挿入できます。

emplaceメンバ関数は、第1引数に挿入位置を指すイテレータを、第2引数以降に、要素のコンストラクタに渡す実引数を指定します。戻り値は、挿入された要素を指すイテレータです。

#include <iostream>
#include <vector>

struct MyStruct {
    MyStruct(int a, const char* b) : mA(a), mB(b)
    {}

    int mA;
    const char* mB;
};

using MyStructVector = std::vector<MyStruct>;

int main()
{
    MyStructVector v;

    const MyStructVector::const_iterator it = v.emplace(v.begin(), 10, "abc");  // MyStruct のコンストラクタの実引数を指定

    std::cout << it->mA << ", " << it->mB << std::endl;
}

実行結果:

10, abc

「容量」が不足していれば自動的に拡張されます。

削除

要素を削除する方法は幾つかありますが、その前に「削除」の意味を確認しておきます。

std::vector から要素を削除するということは、内部にある動的配列から要素を取り除くということです。特に重要な点として、new演算子を使うなどして作ったオブジェクトを、std::vector に追加したとして、その delete を呼ぶ責任は、std::vector の外側にあるということです。つまり、要素を削除する前に、std::vector の外側で delete を行わなければなりません。
これは当然面倒ですし、誤りやすいことなので、スマートポインタを使うことが基本となります。

また、要素が削除されると、「サイズ」が減少します。これは当然ですが、一方で「容量」は減少しません。特に、clearメンバ関数は、すべてが綺麗に消されそうな名前ですが、「サイズ」が 0 になるだけで、「容量」に変化はありません。
「容量」を減らす方法については、「容量に関する処理」を参照して下さい。

pop_back

要素を削除する方法の1つ目は、pop_backメンバ関数です。これは、push_backメンバ関数の逆操作で、末尾にある要素を削除するものです。

例として、std::vector の外側で new演算子によって作られた int型のポインタを扱う場合を示します。冒頭で書いた通り、delete を呼ぶ責任は std::vector の外側にあります。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int*> v;

    v.push_back(new int(10));

    delete v.back();
    v.pop_back();
}

実行結果:



このように、削除の前に要素を取り出して、解放するという手順を踏まねばなりません。これでは解放に関するトラブルを誘発するだけですから、スマートポインタを使うようにします。

#include <iostream>
#include <memory>
#include <vector>

int main()
{
    std::vector<std::unique_ptr<int>> v;

    v.emplace_back(new int(10));
    std::cout << *(v.back()) << std::endl;

    v.pop_back();
}

実行結果:

10

erase

要素を削除するには、eraseメンバ関数を使うこともできます。こちらはより汎用的です。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    IntVector v;

    for (int i = 0; i < 5; ++i) {
        v.push_back(i);
    }

    v.erase(v.begin());  // 先頭要素を削除
    PrintVector(v);

    v.erase(v.begin() + 1, v.end());  // 先頭の次から末尾まで削除
    PrintVector(v);
}

実行結果:

1
2
3
4

1

eraseメンバ関数の使い方は2通りで、イテレータを1つ渡すか、2つ渡すかの違いです。
前者の場合はイテレータが指す要素を削除し、後者の場合は2つのイテレータで作られる範囲内の要素が削除されます。

要素が削除されることによって、その位置より後方にあった要素のメモリアドレスが変わり得るため、それらの要素を指していたポインタ、参照、イテレータが無効になることに注意して下さい。

eraseメンバ関数は、削除された要素の次の有効な要素を指すイテレータが返します。std::vector をループを使って走査し、条件に合う要素だけを削除する場合、次のように書けます。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    const int a[] = {0, 3, 2, 3, 3};
    IntVector v(a, a + 5);

    // 値が 3 の要素をすべて削除する
    for (IntVector::iterator it =  v.begin(); it != v.end(); /* ここでインクリメントしないこと */) {
        if (*it == 3) {
            it = v.erase(it);  // 次の要素を指すイテレータを受け取る
        }
        else {
            ++it;
        }
    }

    PrintVector(v);
}

実行結果:

0
2

インクリメントをする位置に注意して下さい。eraseメンバ関数の戻り値を受け取ってイテレータを更新する場合は、そのイテレータが既に「次の要素」を指しているので、更なるインクリメントをしないようにします。
また、ループの終了条件を確認するため、endメンバ関数の戻り値を毎回取得し直すことにも注意が必要です。人によっては、

const IntVector::iterator itEnd = v.end();
for (IntVector::iterator it = v.begin(); it != itEnd; ) {
}

こういう形でループを書きたいと思うかもしれませんが、これだと要素が削除された瞬間に、endメンバ関数が返したイテレータが無効になるので、正しく動作しません。

このような方法で std::vector から要素を削除するには、かなり気を使って実装しなければなりません。より安全に書くためには、こういった処理は自前で書かずに、STLアルゴリズム第26章)を使うべきです。
STLアルゴリズムの remove関数第29章)を使えば、同じ処理を次のように書けます。

#include <iostream>
#include <vector>
#include <algorithm>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    const int a[] = {0, 3, 2, 3, 3};
    IntVector v(a, a + 5);

    // 値が 3 の要素をすべて削除する
    v.erase(std::remove(v.begin(), v.end(), 3), v.end());

    PrintVector(v);
}

実行結果:

0
2

数ある STLアルゴリズムを覚えたり、調べたりするのは大変ですし面倒かもしれませんが、圧倒的に安全ですし、実は効率的でもあります。

clear

clearメンバ関数は、すべての要素を削除します。

#include <iostream>
#include <vector>
#include <cassert>

int main()
{
    std::vector<int> v;

    for (int i = 0; i < 5; ++i) {
        v.push_back(i);
    }

    v.clear();
    assert(v.empty());
}

実行結果:



比較

同じテンプレート実引数を持った std::vector は、== などの比較演算子を使って比較可能です。

==演算子は、両者の要素を1つ1つ ==演算子を使って比較し、すべてが true であれば true になります。もし両者の「サイズ」が異なっていた場合の結果は false になります。「容量」の違いは無関係です。
!=演算子は、==演算子の逆の結果を返します。

<、<=、>、>= の各演算子は、両者の要素を1つずつ同じ演算子を使って比較し、すべてが true であれば true になります。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    IntVector v1, v2;

    for (int i = 0; i < 5; ++i) {
        v1.push_back(i);
    }

    v2 = v1;
    std::cout << (v1 == v2) << "\n"
              << (v1 != v2) << "\n"
              << (v1 <  v2) << "\n"
              << (v1 <= v2) << "\n"
              << (v1 >  v2) << "\n"
              << (v1 >= v2) << std::endl;

    std::cout << "-----" << std::endl;

    v2[3] = 10;
    std::cout << (v1 == v2) << "\n"
              << (v1 != v2) << "\n"
              << (v1 <  v2) << "\n"
              << (v1 <= v2) << "\n"
              << (v1 >  v2) << "\n"
              << (v1 >= v2) << std::endl;
}

実行結果:

1
0
0
1
0
1
-----
0
1
1
1
0
0

交換

swapメンバ関数を使うと、自身の内容を他の std::vector の内容と交換できます。このとき、「容量」も交換されます。両者のテンプレート実引数は同じでなければなりません。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    IntVector v1, v2;

    for (int i = 0; i < 5; ++i) {
        v1.push_back(i);
    }

    v1.swap(v2);

    PrintVector(v1);
    std::cout << "-----" << std::endl;
    PrintVector(v2);

    std::cout << v1.capacity() << ", "
              << v2.capacity() << std::endl;
}

実行結果:


-----
0
1
2
3
4

0, 6

以下のように宣言された、メンバ関数ではない swap関数もあります。

namespace std {
    template <typename T, typename Allocator>
    void swap(vector<T,Allocator>& a, vector<T,Allocator>& b);
}

C++17 (std::swapメンバ関数の例外仕様)

C++17 からは、例外仕様が追加されて、以下のように宣言されています。

namespace std {
    template <typename T, typename Allocator>
    void swap(vector<T,Allocator>& a, vector<T,Allocator>& b) noexcept(noexcept(lhs.swap(rhs)));
}

第2章で、std::swap関数を取り上げていますが、その実引数に std::vector を指定すると、自動的に、この std::vector専用バージョンが使用されます。

非メンバ関数版の swap は「a.swap(b)」のような形で、メンバ関数版の swap を呼びます。非メンバ関数版は、std::vector以外の型の交換であっても同じ記述になるので、汎用的に使えます。

容量に関する処理

capacity

現在の「容量」は、capacityメンバ関数を使って取得できます。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

int main()
{
    IntVector v1;
    std::cout << v1.capacity() << std::endl;

    IntVector v2(100);
    std::cout << v2.capacity() << std::endl;
}

実行結果:

0
100

reserve

reserveメンバ関数を使うと、容量を増やせます。この関数は、要素を追加しないので、サイズの方は変化しません。

reserveメンバ関数では、容量を小さくすることはできません。もし、現在の容量以下の値を指定した場合には、何も起こりません。容量を減らしたいときは、shrink_to_fitメンバ関数を使います。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

int main()
{
    IntVector v;
    std::cout << v.capacity() << std::endl;

    v.reserve(1000);
    std::cout << v.capacity() << std::endl;

    v.reserve(0);  // 減らすことはできない
    std::cout << v.capacity() << std::endl;
}

実行結果:

0
1000
1000

reserveメンバ関数は主に、容量が拡張される際に行われる、動的配列の再確保の処理によって、パフォーマンスが低下することを避けるために使用されます。

shrink_to_fit

容量を減らすには、shrink_to_fitメンバ関数を使います。この関数は「容量」を、現在の「サイズ」に合わせるように縮小します。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v;
    v.reserve(10000);  // 10000以上の容量を確保する
    v.push_back(0);    // 要素を1つ追加(サイズが 1 増える)

    std::cout << v.capacity() << std::endl;

    v.shrink_to_fit();

    std::cout << v.capacity() << std::endl;
}

実行結果:

10000
1

現在の「サイズ」への縮小なので、「容量」を 0 にしたいのであれば、直前で clearメンバ関数を呼び出して「サイズ」を 0 にする必要があります。

なお、「容量」が縮小する際にも、メモリの再確保が行われる可能性があることに注意して下さい。そのため、要素を指すポインタ、参照、イテレータは無効になるかもしれません。

サイズに関する処理

size

現在の「サイズ」は、sizeメンバ関数で取得できます。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v1;
    std::cout << v1.size() << std::endl;

    std::vector<int> v2(100);
    std::cout << v2.size() << std::endl;
}

実行結果:

0
100

empty


サイズが 0 かどうかを知りたいときは、emptyメンバ関数を使用できます。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v;
    if (v.empty()) {
        std::cout << "empty" << std::endl;
    }
}

実行結果:

empty

max_size

「サイズ」の最大値というものがあり、max_sizeメンバ関数で取得できます。この値が意味するのは、std::vector に格納できる要素数の限界です。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v;
    std::cout << v.max_size() << std::endl;
}

実行結果:

4611686018427387903

resize

resizeメンバ関数を使うと、「サイズ」を変更できます。

現在の「サイズ」よりも大きい数を指定した場合は、指定した数になるように、要素を追加します。このとき「容量」が足りなければ拡張します。
一方、現在の「サイズ」よりも小さい数を指定した場合は、指定した数になるように、末尾側の要素を取り除きます。「容量」の縮小は起こりません。
「サイズ」に変化が起こらない場合は、何も行いません。

resizeメンバ関数には、次の2つの形式があります。

void resize(size_type sz);
void resize(size_type sz, const T& c);

引数sz が変更後の「サイズ」です。1つ目の形式では、要素が追加されるときにデフォルトコンストラクタが使用され、2つ目の形式では、引数c を使ってコピーが作られます。

以下は使用例です。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

namespace {
    void PrintVector(const IntVector& v)
    {
        const IntVector::size_type size = v.size();
        for (IntVector::size_type i = 0; i < size; ++i) {
            std::cout << v[i] << "\n";
        }
        std::cout << std::endl;
    }
}

int main()
{
    IntVector v;

    for (int i = 0; i < 3; ++i) {
        v.push_back(i);
    }
    PrintVector(v);
    std::cout << "-----" << std::endl;

    v.resize(5);
    PrintVector(v);
    std::cout << "-----" << std::endl;

    v.resize(3);
    PrintVector(v);
    std::cout << "-----" << std::endl;

    v.resize(5, 999);
    PrintVector(v);
}

実行結果:

0
1
2

-----
0
1
2
0
0

-----
0
1
2

-----
0
1
2
999
999

イテレータ

イテレータ(反復子)は、データ構造に含まれる各要素に対する操作を抽象化する仕組みです。つまり、データ構造の種類を問わず、同じ方法で要素を操作できます。
イテレータという仕組みは、標準ライブラリ全体の中でも、比較的大きなテーマなので、詳細は、第9章で改めて取り上げることにして、ここでは std::vector を使うために最低限必要なことにだけ触れます。

イテレータは、C言語のポインタに近い感覚の機能になっています。つまり、イテレータは、データ構造に含まれる要素の1つを指し示すものです。そして、ポインタと同じように、*演算子や ->演算子などを使うことができるように作られています。
ただし、「ポインタ」=「イテレータ」では無いということは理解しておいて下さい。std::vector のためのイテレータは、ポインタを使って実装されていることがありますが、必ずそうだということではありません。

std::vector の先頭要素を指すイテレータを beginメンバ関数で、末尾要素の次を指すイテレータを endメンバ関数で取得できます。

クラスに属さない通常関数としての std::begin関数、std::end関数もあり、これらを使ってもイテレータを取得できます。第9章で取り上げます。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

int main()
{
    IntVector v;

    for (int i = 0; i < 5; ++i) {
        v.push_back(i);
    }

    const IntVector::iterator itEnd = v.end();
    for (IntVector::iterator it = v.begin(); it != itEnd; ++it) {
        std::cout << *it << "\n";
    }
    std::cout << std::endl;
}

実行結果:

0
1
2
3
4

beginメンバ関数や endメンバ関数で取得できるイテレータの型は、std::vector<>::iterator型です。要素の操作を抽象化できるといっても、型自体は std::vector に依存しています。

ポインタに constポインタがあるように、イテレータには constイテレータがあります。意味合いは同じで、constイテレータが指し示している先の要素は、変更することができません。
constイテレータは、cbeginメンバ関数cendメンバ関数を使って取得します。これらの関数が返す constイテレータの型は、std::vector<>::const_iterator型です。

C++14以降には、クラスに属さない通常関数としての std::cbegin関数、std::cend関数もあり、これらを使ってもイテレータを取得できます。第9章で取り上げます(std::begin、std::end は C++11 にありますが、std::cbegin、std::cend は C++14 からです)。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

int main()
{
    IntVector v;

    for (int i = 0; i < 5; ++i) {
        v.push_back(i);
    }

    const IntVector::const_iterator itEnd = v.cend();
    for (IntVector::const_iterator it = v.cbegin(); it != itEnd; ++it) {
        std::cout << *it << "\n";
    }
    std::cout << std::endl;
}

実行結果:

0
1
2
3
4

これらイテレータの型は、std::vector内部で定義されているものですが、その正体が何であるかは実装依存です。std::vector の場合は、テンプレート仮引数T に合わせた単なるポインタ (T*型) であることも多いですが、そういう実装ではないこともあります。そのため、次のようなコードには移植性がありません。

int* p = v.begin();

なお、beginメンバ関数と endメンバ関数を持っていることによって、std::vector に対して範囲for文(【言語解説】第3章、および第16章)を使えます。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

int main()
{
    IntVector v;

    for (int i = 0; i < 5; ++i) {
        v.push_back(i);
    }

    for (IntVector::value_type n : v) {
        std::cout << n << "\n";
    }
    std::cout << std::endl;
}

実行結果:

0
1
2
3
4

要素の型はメンバ型の value_type にあるので、これを使えます。

ただし、auto(【言語解説】第20章)を使うのがより簡単で、より一般的です。

逆イテレータ

要素を、末尾側から先頭へ向かって逆方向に走査するためのイテレータもあります。これを逆イテレータと呼びます。

逆イテレータは、rbeginメンバ関数rendメンバ関数で取得できます。
逆イテレータの型は、std::vector<>::reverse_iterator型です。

また、const逆イテレータもあり、こちらは、crbeginメンバ関数crendメンバ関数で取得できます。
const逆イテレータの型は、std::vector<>::const_reverse_iterator型です。

#include <iostream>
#include <vector>

using IntVector = std::vector<int>;

int main()
{
    IntVector v;

    for (int i = 0; i < 5; ++i) {
        v.push_back(i);
    }

    const IntVector::const_reverse_iterator ritEnd = v.crend();
    for (IntVector::const_reverse_iterator rit = v.crbegin(); rit != ritEnd; ++rit) {
        std::cout << *rit << "\n";
    }
    std::cout << std::endl;
}

実行結果:

4
3
2
1
0

生配列との連携

std::vector の内部にある要素を、通常の配列のように扱えると便利なことがあります。例えば、std::vector の要素を配列へコピーしたいとき、memcpy関数のようなコピー関数が使えれば簡単です。

std::vector の内部にある動的配列のメモリアドレスは、次のようにすれば取得できます。

std::vector<int> v(10);

std::vector<int>::pointer p = &v[0];

要素が連続的に並んでいるので、こうして取得したメモリアドレスを使って memcpy関数などの関数を使用できます。ただし、v が空の状態で「v[0]」をすると未定義な動作になってしまうので注意が必要です
ただし、後述する vector<bool> の場合には、このような方法が使えないことに注意して下さい。

あるいは、dataメンバ関数で、内部の配列のメモリアドレスを得られるので、これを使いましょう。

ところで、beginメンバ関数を使って、先頭要素のイテレータを取得する方法では代用できないことに注意して下さい。イテレータ(std::vector<>::iterator型) の正体は、ポインタでは無い可能性があるからです。もし、イテレータを使いたいのなら、次のように書く必要があります。

std::vector<int>::pointer p = &*v.begin();  // *演算子で間接参照後、メモリアドレスを取得

一時バッファとしての利用

要素数が事前に分からないとき、new[] を使って配列を確保しようとすることがあります。

void func(int size)
{
    int* array = new int[size];

    // 何か処理する

    delete [] array;
}

new[] を呼び出した場合、確実に delete[] を呼び出す必要があります。func関数の処理内容が後から増えるたび、注意深く delete[] の呼び出し抜けが無いことを確認しなければなりません。

例えば、if文が増えて途中で return する経路ができるかもしれませんし、例外の送出によって関数を抜け出すことがあるかもしれません。

このような可能性を常に考えに入れながらプログラムを書くことは、大変な労力になります。そこで、std::vector を使用して解放処理を任せてしまいます。

void func(int size)
{
    std::vector<int> v(size);

    // 何か処理する
}

std::vector が破棄されるときに、デストラクタが動的な配列を解放してくれるので、どのような方法で func関数を抜け出すとしても問題ありません。

new[] を使うことはできるだけ避けて、std::vector を使うようにしましょう。

vector<bool>

std::vector のテンプレート仮引数 T に bool を指定した場合、ここまでに取り上げた vector とは異なる特別バージョンになります。これは、テンプレートの特殊化(【言語解説】第29章)という機能を使っているためです。

std::vector<bool> は、要素として bool型を使う std::vector な訳ですが、bool型の動的な配列を作るのではなく、1つの bool値に対して 1ビットだけを使うようにして、メモリ使用量を削減しています。

std::vector<bool> は、通常の std::vector が持っている機能を使える他、追加のメンバ関数もあります。例えば、flipメンバ関数を使うと、要素を反転 (true なら false に、flase なら true に)できます。

#include <iostream>
#include <vector>

int main()
{
    using boolVector = std::vector<bool>;

    boolVector v(5);

    v[3] = true;
    v.flip();

    for (boolVector::size_type i = 0; i < v.size(); ++i) {
        std::cout << std::boolalpha << v[i] << std::endl;
    }
}

実行結果:

true
true
true
false
true

std::vector<bool> の要素の初期値のデフォルトは false です。

また、添字演算子が返す参照から、flip関数を呼び出せます。

#include <iostream>
#include <vector>

int main()
{
    using boolVector = std::vector<bool>;

    boolVector v(5);

    v[3].flip();

    for (boolVector::size_type i = 0; i < v.size(); ++i) {
        std::cout << std::boolalpha << v[i] << std::endl;
    }
}

実行結果:

false
false
false
true
false

普通、[]演算子が返すのは、要素への参照ですから bool& のように思えます。しかし、flipメンバ関数を呼び出せているように、[]演算子は bool& を返してはいないようです。

こんなことができるのは、[]演算子が、std::vector<bool> の内部にある補助クラスオブジェクトへの参照を返しているからです。詳しい内容は割愛しますが、この例で呼び出している flip関数を持っているのは、この補助クラスの方です。

このような補助クラスはプロキシ(代理)と呼ばれます。デザインパターンでいうところの Proxyパターンです。有用なテクニックなので、Proxyパターンについて調べてみるか、実際に std::vector<bool> の実装を覗いてみると良いでしょう。

このように、std::vector<bool> は特殊な実装であるため、挙動が特殊な部分があり、使い方に注意が必要です。例えば、次のコードがコンパイルできません。

std::vector<bool> v;
bool* p = &v[0];

これは、先ほどの話の通り、[]演算子が返すものは bool& では無いからです。キャストで無理やりコンパイルすればいいということでもありません。

また、次の assert も失敗してしまいます。

std::vector<bool> v;
assert(&v[0] + 1 == &v[1]);  // 失敗

こういった問題があるため、std::vector<T> を扱う関数を作ったとしても、T が bool のときにだけコンパイルできなかったり、おかしな動作を起こしたりするかもしれません。

このような混乱があるため、std::vector<bool> の挙動をきちんと理解しているならば、使っていけないということはありませんが、基本的には使わずに済むのなら使わない方がいいと言われています

もし、必要なビット数が静的に決定できるのであれば、代わりに std::bitset(第26章)を使って下さい。
動的でなければ決定できない場合で、メモリ使用量の削減が目的でないのなら(単に bool型のコレクションが欲しいだけ)、std::deque(第18章) を使うのが無難です
。 メモリ使用量の削減が目的であるならば、慎重に std::vector<bool> を使うか、別のクラスを用意することになります。

動的に大きさを決定できる bitset の実装として、boost の dynamic_bitset があります。boost は、非標準ですが、非常に有名な C++ のライブラリです。


練習問題

問題① あなたの使っているコンパイラにおいて、空の std::vector はどれだけのサイズと容量を持つか調べてみて下さい。

問題② 次のプログラムには、std::vector の使い方に関する幾つかの問題が潜んでいます。指摘して下さい。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v;

    v[3] = 3;

    v.resize(100);

    int* p = &v[10];

    v.insert(v.begin(), 200);

    *p = 10;
}

問題③ この章のサンプルプログラム内で何度か登場した PrintVector関数を、イテレータを使った方法で書き直して下さい。範囲for文を使わない場合と、使う場合とでそれぞれ書いてみて下さい。


解答ページはこちら

参考リンク



更新履歴

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

'2017/11/20 新規作成。



前の章へ(第5章 weak_ptr)

次の章へ(第7章 array)

Modern C++編のトップページへ

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー