array | Programming Place Plus Modern C++編【標準ライブラリ】 第7章

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要

この章の概要です。


概要

std::array は、要素数が固定された配列を表現したクラステンプレートです

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

メンバ変数に、生の配列を保持しているほか、配列操作のために便利な機能を多数備えています。生の配列をそのまま持っているので、各要素がメモリ上に連続的に並ぶことが保証されています。

std::array は動的な確保を行わないので、要素数が固定で構わないのであれば、std::vector(第6章)よりもメモリ使用量や、実行速度の面でも有利です。余計なメンバ変数もありませんから、通常の配列と同じ性能を維持しているといって良いでしょう。

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

namespace std {
    template <typename T, size_t N>
    struct array {
    };
}

テンプレート仮引数 T は配列の要素の型、N はサイズ(要素数)です。たとえば、int型で要素数 100 の配列を、次のようにして得られます。

std::array<int, 100> intArray;


生成と初期化

std::array には、明示的に宣言されたコンストラクタがありません。

std::array は生の配列を保持していますが、実はこれは「公開」されたメンバ変数になっています。たとえば以下のように定義されています。

namespace std {
    template <typename T, size_t N>
    struct array {
        T elems[N];  // 名前は実装依存

        // その他、多数のメンバがあるが、
        // コンストラクタは無い。
    };
}

公開されているとはいえ、内部配列の名前は実装依存なので、名前が必要になるような使い方をすべきではありません。


std::array は集成体であるため、リスト初期化が使用できます(【言語解説】第16章)。つまり、配列と同じように、{ } を使って各メンバの初期値を指定する構文が利用できます。

#include <array>

int main()
{
    std::array<int, 5> a = {0, 1, 2, 3, 4};
}

初期化時に「=」を使う、コピー初期化の記法であればこれで問題ありませんが、「=」を使わない直接初期化の記法の場合は、{ } を2組使う必要があります。

#include <array>

int main()
{
    std::array<int, 5> a {{0, 1, 2, 3, 4}};  // C++11 では { } が2組必要
}

この2組の {} は、集成体である std::array そのものに与える初期化リストと、メンバ変数の配列に与える初期化リストの2つを表しています。

明示的なコンストラクタがないので、コンパイラが自動生成するデフォルトコンストラクタを使えますから、初期値を与えずに生成できます。

#include <array>

int main()
{
    std::array<int, 5> a;  // 要素は未初期化
}

この場合は、内部配列は初期化されず、不定値が入ったままです。std::array はこの点においては、生の配列と危険性が変わっていないことに注意してください。

要素をきちんと初期化するためには、空の { } を与えると良いです。こうすると、各要素はデフォルトの方法で初期化されます。つまり、明示的に定義されたデフォルトコンストラクタがあればそれを呼び、なければ 0 による初期化を試みます。

#include <array>

int main()
{
    std::array<int, 5> a = {};  // 各要素の値は 0
}

破棄

std::array は、終了処理が必要なメンバを持っていないので、デストラクタは何も行いません。

仮想デストラクタではないので、 std::array の派生クラスを定義して使用することは、不適切な場合があるので注意してください

メンバ型

std::array には、いくつかの型名が「公開」されたメンバとして定義されています。以下、表にまとめて紹介しますが、具体的にどう定義されているかは、実装によって異なります。

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

メンバ型名 意味
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逆イテレータ型

要素のアクセス

要素へアクセスする方法はいくつかあります。ここで取り上げる各メンバ関数([]演算子もメンバ関数です)はそれぞれ、constメンバ関数版と、非メンバ関数版とがあります。

また、std::get関数テンプレートを使う方法がありますが、これは「タプルとしての操作」の項で取り上げます。

[]演算子

std::array は [] を使った添字アクセスが可能です。

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

int main()
{
    IntArray a;

    a[3] = 10;
    IntArray::value_type v = a[3];

    std::cout << v << std::endl;
}

実行結果:

10

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

at

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

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

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

int main()
{
    IntArray a;

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

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

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

実行結果:

10
invalid array<T, N> subscript

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

front、back

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

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

int main()
{
    IntArray a;

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

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

実行結果:

0
4

data

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

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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()
{
    IntArray a;

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

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

実行結果:

0
1
2
3
4

dataメンバ関数は、非constメンバ関数版と、constメンバ関数版とがあります。前者は std::array<>::pointer型、後者は std::array<>::const_pointer型で返します。

std::array の内部にある配列は、その要素がメモリ上に並んでいることが保証されているので、このサンプルプログラムのように、普通の配列の先頭メモリアドレスが返されたと考えて使用できます。

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

代入

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

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

int main()
{
    IntArray a;

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

    IntArray a2;
    a2 = a;  // a のコピー
    PrintArray(a);

    a2 = { 0, 1, 2 };  // 0, 1, 2 の 3つを代入
    PrintArray(a);
}

実行結果:

0
1
2
3
4

0
1
2
3
4

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

#include <array>
#include <iostream>
#include <memory>

using PtrArray = std::array<std::unique_ptr<int>, 5>;

namespace {
    void PrintArray(const PtrArray& a)
    {
        const PtrArray::size_type size = a.size();
        for (PtrArray::size_type i = 0; i < size; ++i) {
            if (a[i] == nullptr) {
                std::cout << "null" << "\n";
            }
            else {
                std::cout << *(a[i]) << "\n";
            }
        }
        std::cout << std::endl;
    }
}

int main()
{
    PtrArray a1 = {
        std::unique_ptr<int>(new int(5)),
        std::unique_ptr<int>(new int(10)),
        nullptr,
        std::unique_ptr<int>(new int(5)),
        std::unique_ptr<int>(new int(10))
    };
    PtrArray a2;

    a2 = std::move(a1);
    PrintArray(a1);
    PrintArray(a2);
}

実行結果:

null
null
null
null
null

5
10
null
5
10

特定の値で埋める

fillメンバ関数を使うと、すべての要素を特定の値で埋められます。

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

int main()
{
    IntArray a;
    a.fill(7);
    PrintArray(a);
}

実行結果:

7
7
7
7
7

比較

同じテンプレート実引数を持った std::array は、等価演算子や関係演算子を使って比較が可能です。

==演算子は、両者の要素を1つ1つ ==演算子を使って比較し、すべてが true であれば true になります。サイズが異なると、テンプレート実引数も異なっていますから、そもそも比較できません。!=演算子は、==演算子の逆の結果を返します。

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

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

int main()
{
    IntArray a1 = {0, 1, 2, 3, 4};
    IntArray a2;

    a2 = a1;
    std::cout << (a1 == a2) << "\n"
              << (a1 != a2) << "\n"
              << (a1 <  a2) << "\n"
              << (a1 <= a2) << "\n"
              << (a1 >  a2) << "\n"
              << (a1 >= a2) << std::endl;

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

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

実行結果:

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

交換

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

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

int main()
{
    IntArray a1 = {0, 1, 2, 3, 4};
    IntArray a2 = {7, 7, 7, 7, 7};

    a1.swap(a2);

    PrintArray(a1);
    std::cout << "-----" << std::endl;
    PrintArray(a2);
}

実行結果:

7
7
7
7
7

-----
0
1
2
3
4

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

namespace std {
    template <typename T, size_t N>
    void swap(array<T,N>& lhs, array<T,N>& rhs);
}

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

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

サイズに関する関数

サイズ(要素数)に関する関数を取り上げます。

size

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

sizeメンバ関数は、constexpr関数(【言語解説】第24章)です。

#include <array>
#include <iostream>

int main()
{
    std::array<int, 10> a1;
    std::cout << a1.size() << std::endl;

    std::array<int, 100> a2;
    std::cout << a2.size() << std::endl;

    std::array<int, 0> a3;
    std::cout << a3.size() << std::endl;
}

実行結果:

10
100
0

std::array のサイズは、テンプレート実引数で指定しており、静的に定まっています。しかし、size関数は普通のメンバ関数なので、std:array のオブジェクトを作らないと呼び出すことができません。サイズをコンパイル時に使いたいときは、std::tuple_size関数を使う方法があります。これは、後程取り上げます

empty

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

emptyメンバ関数は、constexpr関数(【言語解説】第24章)です。

#include <array>
#include <iostream>

int main()
{
    std::array<int, 0> a;
    if (a.empty()) {
        std::cout << "empty" << std::endl;
    }
}

実行結果:

empty

std::array の場合、2つ目のテンプレート仮引数に 0 を指定しない限り、サイズが 0 になる状況は起こり得ません。

max_size

「サイズ」の最大値というものがあり、max_sizeメンバ関数で取得できます。この値が意味するのは、std::array に格納できる要素数の限界です。std::array にとってのそれは、2つ目のテンプレート仮引数に指定した値と同じです。

max_size関数は constexpr関数(【言語解説】第24章)です。

#include <array>
#include <iostream>

int main()
{
    std::array<int, 10> a1;
    std::cout << a1.max_size() << std::endl;

    std::array<int, 100> a2;
    std::cout << a2.max_size() << std::endl;

    std::array<int, 0> a3;
    std::cout << a3.max_size() << std::endl;
}

実行結果:

10
100
0

イテレータ

イテレータという概念に関する詳細は、第9章であらためて取り上げます。

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

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

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

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

実行結果:

0
1
2
3
4

beginメンバ関数や endメンバ関数で取得できるイテレータの型は、std::array<>::iterator型です。

ポインタに constポインタがあるように、イテレータには constイテレータがあります。意味合いは同じで、constイテレータが指し示している先の要素は、変更できません。

constイテレータは、cbeginメンバ関数cendメンバ関数を使って取得できます。そのほか、beginメンバ関数や endメンバ関数の constメンバ関数版を使えば、constイテレータが返されます。これらの関数が返す constイテレータの型は、std::array<>::const_iterator型です。

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

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

実行結果:

0
1
2
3
4

これらイテレータの型は、std::array内部で定義されているものですが、その正体が何であるかは実装依存です

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

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

    for (IntArray::value_type n : a) {
        std::cout << n << "\n";
    }
    std::cout << std::endl;
}

実行結果:

0
1
2
3
4

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

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

逆イテレータ

要素を、末尾側から先頭へ向かって逆方向に走査する逆イテレータは、rbeginメンバ関数rendメンバ関数で取得できます。

逆イテレータの型は、std::array<>::reverse_iterator型です。

また、const逆イテレータもあり、こちらは、crbeginメンバ関数crendメンバ関数で取得できます。そのほか、rbeginメンバ関数や rendメンバ関数の constメンバ関数版を使えば、constイテレータが返されます。

const逆イテレータの型は、std::array<>::const_reverse_iterator型です。

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

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

実行結果:

4
3
2
1
0

生配列との連携

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

std::array<int> a = {0, 1, 2};

std::array<int>::pointer p = &a[0];

要素が連続的に並んでいるので、こうして取得したメモリアドレスを使って memcpy関数などの関数を使用できます。ただし、a が空の状態で「a[0]」をすると未定義な動作になってしまうので注意が必要です

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

std::array<int> a = {0, 1, 2};

std::array<int>::pointer p = a.data();

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

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

タプルとしての操作

型を問わず、複数の値を扱う構造を、タプル(組)と呼びます。タプルに対する操作の一部は、共通化された方法で行うことができるようになっていますが、これらの操作を std::array に対しても適用できます。つまり、std::array をタプルとみなして操作できます。

std::tuple_size

std::tuple_size は、タプルの要素数を得るためのクラステンプレートです。関数ではありません。

テンプレート実引数に std::array を指定し、value という staticメンバ変数の値を見ると、そこにタプルの要素数が入っています。型は std::size_t型です。

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

    std::cout << std::tuple_size<IntArray>::value << std::endl;
}

実行結果:

5

sizeメンバ関数を使う場合と違って、静的に要素数を得られます。

std::tuple_element

std::tuple_element は、タプルの要素の型を得るためのクラステンプレートです。関数ではありません。

1つ目のテンプレート実引数に、何番目の要素の型を得たいのかを指定し、2つ目のテンプレート実引数に std::array を指定します。type という名前の型メンバ (using で定義された型名)があるので、これを見れば、要素の型が得られます。

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

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

    std::tuple_element<2, IntArray>::type n = a[2];
    std::cout << n << std::endl;
}

実行結果:

2

std::get

std::get は、タプルの要素への参照を得るための関数テンプレートです。

テンプレート仮引数は3つありますが、後続の2つは、実引数から自動的に判断できるので、通常は最初の1つだけを明示的に指定します。1つ目のテンプレート仮引数は、何番目の要素かを指定します。

また、仮引数は std::array の左辺値参照(const参照も可)、あるいは右辺値参照です。

戻り値の型は、基本的には要素の参照型です。実引数に左辺値参照を渡したなら、戻り値も左辺値参照、実引数が右辺値参照なら、戻り値も右辺値参照です。左辺値参照ならば const が付くこともあります。

#include <array>
#include <iostream>

using IntArray = std::array<int, 5>;

int main()
{
    IntArray a = {0, 1, 2, 3, 4};

    std::get<1>(a) = 9;
    std::cout << std::get<1>(a) << std::endl;
}

実行結果:

9


練習問題

問題① std::array の各要素の値を標準出力へ出力する関数テンプレートを作成してください。要素の型と要素数がどのように指定されていても使用できるように実装してください。どのようなフォーマットで出力するかは自由に決めて構いません。

問題② 要素の型が T、要素数が N の std::array を生成して返す関数テンプレートを作成してください。なお、実引数には、すべての要素に与える共通の初期値を指定できるようにしてください。


解答ページはこちら

参考リンク


更新履歴

’2018/7/13 サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)

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

’2018/1/5 Xcode 8.3.3 を clang 5.0.0 に置き換え。

’2017/12/9 C++11 で、初期化時の {} が2組必要であったのは、直接初期化に限られるようなので、それに合わせて解説とサンプルプログラムを修正。

’2017/11/30 新規作成。



前の章へ (第6章 vector)

次の章へ (第8章 initializer_list)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る