ノンタイプテンプレート | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、テンプレート引数が型ではなく値であるノンタイプテンプレートを取り上げます。型を取り換えられるこれまでのテンプレートを理解していれば、考え方はそれほど違うわけではありません。標準ライブラリでの代表的な利用例として、std::array と std::bitset についても紹介します。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



ノンタイプテンプレート 🔗

これまでのページで登場したテンプレートでは、テンプレート実引数に渡すものは型でした。しかし実は、型ではなく、値を渡すことも可能です。値を指定するテンプレート仮引数を、ノンタイプテンプレート仮引数(非型テンプレート仮引数) (non-type template parameter) といい、テンプレート実引数の側を、ノンタイプテンプレート実引数(非型テンプレート実引数) (non-type template argument) といいます。テンプレート引数にノンタイプな引数があるテンプレートを、ノンタイプテンプレート(非型テンプレート) (non-type template) と呼びます。

考え方は通常の型によるテンプレート引数と同じで、クラステンプレートや関数テンプレートの中で、ノンタイプテンプレート仮引数の名前を使うことができます。その部分はいわばテンプレートの空欄部分であり、そこを埋めるのがノンタイプテンプレート実引数の役目となります。

ノンタイプテンプレート引数は、クラステンプレート、関数テンプレートのどちらでも使えます。また、デフォルトテンプレート実引数(「クラステンプレート」「関数テンプレート」のページを参照)を持たせることもできます。ノンタイプテンプレート引数と、通常の型のテンプレート引数を混ぜて使うことも可能です。

ノンタイプテンプレート仮引数

テンプレートを宣言する構文は次のようになっていました。

// クラステンプレートの場合
template <テンプレート仮引数の宣言の並び>
class クラステンプレート名 {
    メンバの宣言;
      :
};

// 関数テンプレートの場合
template <テンプレート仮引数の宣言の並び>
戻り値の型 関数テンプレートの名前(仮引数の並び);

ここで「テンプレート仮引数の宣言の並び」のところに、typename や class を使ってテンプレート仮引数を , 区切りで記述すると、型によるテンプレート仮引数になりました。ノンタイプテンプレート仮引数の場合はこれらのキーワードの代わりに、受け取れる値の型名を記述します。

型名 ノンタイプテンプレート仮引数の名前

たとえば次のようになります。

template <int N1, int N2>
class C {
private:
    int m_data1[N1];
    int m_data2[N2];
};

ノンタイプテンプレート仮引数に使えるのは以下のいずれかの型です[1]

型の詳細については、「APPENDIX > 型の分類表」を参照してください。

(C++14 では)浮動小数点数型が使えないことに注意が必要です。そのほかクラス型や配列型など、使えないものがあります(配列型は記述できますが、実際にはポインタ型に変換されます)。

【C++17】「プレースホルダー型を含む型」が使えるようになりました。プレースホルダー型とは auto、decltype(auto) のことで、これを含む型として auto*auto& のようなものも許されます[2]。これらを指定した場合、実際の型はテンプレート実引数から推論されます。

【C++20】使える型のリストが整理され、構造的型 (structural type)、プレースホルダー型を含む型、推論用クラスのプレースホルダーの3つに分類されました[3]。構造的型とはスカラ型、参照型、リテラルクラス型のいずれかのことです。スカラ型の中に浮動小数点型があるので、結果的に浮動小数点型、推論用クラスのプレースホルダー、リテラルクラス型が新たに使えるようになったことになります。

ノンタイプテンプレート実引数

ノンタイプテンプレート実引数としては、ノンタイプテンプレート仮引数の型に応じて、以下のようにやや複雑な条件が存在しています[4]

コンパイル時にテンプレートのインスタンス化を行うので、定数でなければなりません。また、メモリアドレスを渡すときの条件として構文が固定されており、p + 1&a[10] のようなものも許されません。

「ノンタイプテンプレート仮引数の名前」は分かりづらいですが、ほかのテンプレートのノンタイプテンプレート実引数として、自身のノンタイプテンプレート仮引数の名前を使っていいということです。

template <int X>
class C1 {};

template <int N>
class C2 {
    C1<N> c;    // C1 のテンプレート実引数に、ノンタイプテンプレート仮引数の名前 (N) を使っている
};

型のテンプレート実引数と同様に(「クラステンプレート」のページを参照)、C1<N> のような記述でノンタイプテンプレート実引数を指定します。ノンタイプテンプレート実引数を指定することによって、テンプレートがインスタンス化されます。

次のように、ノンタイプテンプレート仮引数があるクラステンプレートをインスタンス化したとします。

template <int N>
class C {
private:
    int m_data[N];
};

C<100> c1 {};
C<500> c2 {};

C<100> からは次のインスタンスが、

class C {
private:
    int m_data[100];
};

C<500> から次のインスタンスが生成されます。

class C {
private:
    int m_data[500];
};

このように異なった内容のクラスが作られるため、C<100>C<500> は型としても別物ということになります。テンプレートID が異なると表現してもいいです(「クラステンプレート」のページを参照)。

関数テンプレートでの利用

関数テンプレートでノンタイプテンプレート引数を使用する例として、配列の要素数を調べる関数の実装があります。以前「配列」のページでは、以下のように関数形式マクロを使って実装しました。

#include <iostream>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

int main()
{
    int array1[] {3, 6};
    int array2[] {5, 7, 6, 3, 6};

    std::cout << SIZE_OF_ARRAY(array1) << "\n";
    std::cout << SIZE_OF_ARRAY(array2) << "\n";
}

実行結果:

2
5

基本的にマクロの使用は避けたいので関数にできるといいですが、そのためにノンタイプ関数テンプレートが使えます。

#include <iostream>

template <typename T, std::size_t SIZE>
inline std::size_t size_of_array(const T (&array)[SIZE])
{
    return SIZE;
}

int main()
{
    int array1[] {3, 5};
    int array2[] {5, 7, 6, 3, 6};

    std::cout << size_of_array(array1) << "\n";
    std::cout << size_of_array(array2) << "\n";
}

実行結果:

2
5

関数テンプレートにはテンプレート実引数推論の機能があるため(「関数テンプレート」のページを参照)、仮引数に T と SIZE を登場させていれば、size_of_array(array1) のように呼び出すときの実引数から、テンプレート仮引数 T の型と、SIZE の値がともに推論できるという仕組みです。推論された SIZE の値が要素数ですから、それをそのまま return するだけです。

【C++17】std::size関数として標準ライブラリで定義されました。[5]

std::array 🔗

標準ライブラリでもノンタイプテンプレートが使われています。その1つが std::array です[6]。std::array は、要素数がコンパイル時点で決定されている配列をクラステンプレートにしたもので、<array> に以下のように定義されています。

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

テンプレート仮引数T が要素の型、N が要素数です。あまり使いどころはないですが、N に 0 を与えることも許されています。

std::array の主旨は生の配列の処理効率の高さや、メモリ消費の効率の高さを保ったまま、便利な機能を提供することです。もちろん、要素はメモリ上に連続的に隙間なく並びます。

std::array は struct として定義されており、メンバはすべて public であり、コンストラクタもありません。これは集成体の要件を満たしており(「構造体」のページを参照)、生の配列と同様の構文で初期化(集成体初期化)が行えます(「配列」のページを参照)。しかしそのため、初期化子なしで変数宣言すると、要素は初期化されないことに注意が必要です

#include <array>

int main()
{
    std::array<int, 10> a1 {1, 3, 5};  // 先頭3つの要素は与えた初期化子で初期化。4つ目以降は値初期化される
    std::array<int, 10> a2 = a1;       // a1 の内容をコピー
    std::array<int, 10> a3;            // 注意。要素は未初期化
    std::array<int, 10> a4 {};         // 要素はすべて値初期化
}

要素のアクセスは []atメンバ関数で行います。前者は範囲外アクセスをチェックせず、後者はチェックします。

#include <array>
#include <iostream>

int main()
{
    std::array<int, 10> a {1, 3, 5};

    a[3] = 7;
    std::cout << a.at(3) << "\n";
}

実行結果:

7

イテレータ、constイテレータ、逆イテレータ、const逆イテレータが提供されており、範囲for文を使うこともできます。

#include <array>
#include <iostream>

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

    for (int v : a) {
        std::cout << v << "\n";
    }

    for (auto it = std::begin(a); it != std::end(a); ++it) {
        *it *= 10;
    }

    for (auto rit = std::crbegin(a); rit != std::crend(a); ++rit) {
        std::cout << *rit << "\n";
    }

}

実行結果:

0
1
2
3
4
40
30
20
10
0

型メンバとして以下が定義されています。

名前 意味
iterator イテレータ
const_iterator constイテレータ
reverse_iterator 逆イテレータ
const_reverse_iterator const逆イテレータ
pointer 要素のポインタ型
const_pointer 要素の constポインタ型
reference 要素の参照型
const_reference 要素の const参照型
value_type 要素の型
size_type 要素数を表す型
difference_type 要素間の距離を表す型

先頭要素の参照を frontメンバ関数で、末尾要素の参照を backメンバ関数で取得できます。これらのメンバ関数は、配列が空の場合には未定義動作になります。空かどうかは emptyメンバ関数で調べられますが、std::array は要素数が固定されているので、テンプレート仮引数N を 0 にしないかぎり、空ということはありえません。

#include <array>
#include <iostream>

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

    std::cout << std::boolalpha << a1.empty() << "\n";
    std::cout << a1.front() << "\n";
    std::cout << a1.back() << "\n";

    std::cout << std::boolalpha << a2.empty() << "\n";
}

実行結果:

false
0
4
true

要素数を sizeメンバ関数で取得できます。テンプレート仮引数 N に与えた定数がそのまま返ります。

#include <array>
#include <iostream>

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

    std::cout << a1.size() << "\n";
    std::cout << a2.size() << "\n";
}

実行結果:

5
0

dataメンバ関数で、内部にある生の配列へのポインタを受け取れます。空の配列の場合、どのような結果になるかは未規定です。

#include <array>
#include <iostream>

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

    const int* p {a.data()};
    for (int i{0}; i < a.size(); ++i) {
        std::cout << p[i] << "\n";
    }
}

実行結果:

0
1
2
3
4

fillメンバ関数を使うと、すべての要素に同じ値を与えられます。

#include <array>
#include <iostream>

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

    a.fill(-1);
    for (int v : a) {
        std::cout << v << "\n";
    }
}

実行結果:

-1
-1
-1
-1
-1

同じ型の std::array 同士は比較できます。

#include <array>
#include <iostream>

int main()
{
    std::array<int, 5> a1 {0, 1, 2, 3, 4};
    std::array<int, 5> a2 {0, 10, 20, 30, 40};

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

実行結果:

false
true
true
true
false
false

生の配列と違って、ポインタ型に変換されてしまうことがないので(「配列とポインタ」のページを参照)、関数に渡すときや返すときにも std::array のままで扱えます。

#include <array>
#include <iostream>

void print_array(const std::array<int, 5>& array)
{
    if (array.empty()) {
        return;
    }

    std::size_t i {0};
    while (i < array.size() - 1) {
        std::cout << array[i] << ", ";
        ++i;
    }
    std::cout << array[i] << "\n";
}

int main()
{
    std::array<int, 5> a1 {0, 1, 2, 3, 4};
    std::array<int, 5> a2 {0, 10, 20, 30, 40};

    print_array(a1);
    print_array(a2);
}

実行結果:

0, 1, 2, 3, 4
0, 10, 20, 30, 40

しかしこの方法では、要素の型や要素数が異なる std::array が渡せません。print_array関数をノンタイプ関数テンプレートを使って書くと、テンプレート実引数に指定した内容を問わずに呼び出せるようになります。

#include <array>
#include <iostream>

template <typename T, std::size_t N>
void print_array(const std::array<T, N>& array)
{
    if (array.empty()) {
        return;
    }

    std::size_t i {0};
    while (i < N - 1) {
        std::cout << array[i] << ", ";
        ++i;
    }
    std::cout << array[i] << "\n";
}

int main()
{
    std::array<int, 5> a1 {0, 1, 2, 3, 4};
    std::array<double, 8> a2 {2.2, 2.4, 3.1, 3.3, 3.7, 5.0, 6.5, 7.2};

    print_array(a1);
    print_array(a2);
}

実行結果:

0, 1, 2, 3, 4
2.2, 2.4, 3.1, 3.3, 3.7, 5, 6.5, 7.2

なお、std::sort関数など、イテレータを使うさまざまな関数は std::array を対象にして使うことができます。

【C++20】std::to_array関数を使って、生の配列を std::array に変換できます[7]

リングバッファの内部コンテナに std::array を使う

RingBufferクラステンプレートを改造して、次のコードのように、容量をノンタイプテンプレート実引数で指定できるようにしてみます。

mylib::RingBuffer<int, 100> rb {};

最新の RingBuffer の実装は「chrono」のページにあります。これをベースに、ノンタイプテンプレート仮引数 SIZE を追加します。そして、内部で使っているコンテナを std::vector から std::array に置き換えます。

// ring_buffer.h
#ifndef RING_BUFFER_H_INCLUDED
#define RING_BUFFER_H_INCLUDED

#include <algorithm>
#include <array>
#include <cassert>

namespace mylib {

    // リングバッファ
    template <typename T, std::size_t SIZE>
    class RingBuffer {

        template <typename T, std::size_t SIZE>
        friend class RingBuffer;

    public:
        // イテレータ
        class Iterator {
        public:
            using value_type = T;                       // 要素型
            using reference = value_type&;              // 要素の参照型
            using const_reference = const value_type&;  // 要素の参照型
            using pointer = value_type*;                // 要素のポインタ型
            using const_pointer = const value_type*;    // 要素の constポインタ型
            using size_type = std::size_t;              // サイズ型
            using difference_type = std::ptrdiff_t;     // 距離型

        public:
            // コンストラクタ
            Iterator(RingBuffer& body, size_type pos, bool is_past_the_end);


            // ==演算子
            inline bool operator==(const Iterator& rhs) const
            {
                return m_body == rhs.m_body
                    && m_pos == rhs.m_pos
                    && m_is_past_the_end == rhs.m_is_past_the_end;
            }

            // !=演算子
            inline bool operator!=(const Iterator& rhs) const
            {
                return !(*this == rhs);
            }

            // *演算子(間接参照)
            inline reference operator*()
            {
                return *common_get_elem_ptr(this);
            }

            // *演算子(間接参照)
            inline const_reference operator*() const
            {
                return *common_get_elem_ptr(this);
            }

            // ->演算子
            inline pointer operator->()
            {
                return common_get_elem_ptr(this);
            }

            // ->演算子
            inline const_pointer operator->() const
            {
                return common_get_elem_ptr(this);
            }

            // ++演算子(前置)
            Iterator& operator++();

            // ++演算子(後置)
            Iterator operator++(int);


        private:
            template <typename T>
            inline static auto* common_get_elem_ptr(T* self)
            {
                return &self->m_body.m_data[self->m_pos];
            }

        private:
            RingBuffer&             m_body;             // 本体のリングバッファへの参照
            size_type               m_pos;              // 指し示している要素の位置
            bool                    m_is_past_the_end;  // 終端の次を指すイテレータか
        };

        // constイテレータ
        class ConstIterator {
        public:
            using value_type = T;                       // 要素型
            using const_reference = const value_type&;  // 要素の参照型
            using const_pointer = const value_type*;    // 要素の constポインタ型
            using size_type = std::size_t;              // サイズ型
            using difference_type = std::ptrdiff_t;     // 距離型

        public:
            // コンストラクタ
            ConstIterator(const RingBuffer& body, size_type pos, bool is_past_the_end);


            // ==演算子
            inline bool operator==(const ConstIterator& rhs) const
            {
                return m_body == rhs.m_body
                    && m_pos == rhs.m_pos
                    && m_is_past_the_end == rhs.m_is_past_the_end;
            }

            // !=演算子
            inline bool operator!=(const ConstIterator& rhs) const
            {
                return !(*this == rhs);
            }

            // *演算子(間接参照)
            inline const_reference operator*() const
            {
                return *common_get_elem_ptr(this);
            }

            // ->演算子
            inline const_pointer operator->() const
            {
                return common_get_elem_ptr(this);
            }

            // ++演算子(前置)
            ConstIterator& operator++();

            // ++演算子(後置)
            ConstIterator operator++(int);


        private:
            template <typename T>
            inline static auto* common_get_elem_ptr(T* self)
            {
                return &self->m_body.m_data[self->m_pos];
            }

        private:
            const RingBuffer&       m_body;             // 本体のリングバッファへの参照
            size_type               m_pos;              // 指し示している要素の位置
            bool                    m_is_past_the_end;  // 終端の次を指すイテレータか
        };

    public:
        using container_type = typename std::array<T, SIZE>;                // 内部コンテナの型
        using value_type = typename container_type::value_type;             // 要素型
        using reference = typename container_type::reference;               // 要素の参照型
        using const_reference = typename container_type::const_reference;   // 要素の const参照型
        using pointer = typename container_type::pointer;                   // 要素のポインタ型
        using const_pointer = typename container_type::const_pointer;       // 要素の constポインタ型
        using size_type = typename container_type::size_type;               // サイズ型
        using iterator = Iterator;                                          // イテレータ型
        using const_iterator = ConstIterator;                               // constイテレータ型

    public:
        // コンストラクタ
        RingBuffer() = default;

        // テンプレート変換コンストラクタ
        template <typename U, std::size_t SIZE2>
        RingBuffer(const RingBuffer<U, SIZE2>& other);


        // コピー代入演算子
        RingBuffer& operator=(const RingBuffer& rhs) = default;
        
        // ==演算子
        bool operator==(const RingBuffer& rhs) const;

        // !=演算子
        inline bool operator!=(const RingBuffer& rhs) const
        {
            return !(*this == rhs);
        }


        // 要素を追加
        void push_back(const value_type& value);

        // 要素を取り除く
        void pop_front();

        // 空にする
        void clear();


        // 先頭の要素の参照を返す
        inline reference front()
        {
            return common_front(this);
        }

        // 先頭の要素の参照を返す
        inline const_reference front() const
        {
            return common_front(this);
        }

        // 末尾の要素の参照を返す
        inline reference back()
        {
            return common_back(this);
        }

        // 末尾の要素の参照を返す
        inline const_reference back() const
        {
            return common_back(this);
        }



        // 先頭の要素を指す イテレータを返す
        inline iterator begin()
        {
            return iterator(*this, m_front, empty());
        }

        // 末尾の要素の次を指す イテレータを返す
        inline iterator end()
        {
            return iterator(*this, m_back, true);
        }

        // 先頭の要素を指す constイテレータを返す
        inline const_iterator begin() const
        {
            return const_iterator(*this, m_front, empty());
        }

        // 末尾の要素の次を指す constイテレータを返す
        inline const_iterator end() const
        {
            return const_iterator(*this, m_back, true);
        }


        // 容量を返す
        inline size_type capacity() const
        {
            return m_data.size();
        }

        // 要素数を返す
        inline size_type size() const
        {
            return m_size;
        }

        // 空かどうかを返す
        inline bool empty() const
        {
            return m_size == 0;
        }

        // 満杯かどうかを返す
        inline bool full() const
        {
            return m_size == capacity();
        }

    private:
        // 次の位置を返す
        inline size_type get_next_pos(size_type pos) const
        {
            return (pos + 1) % capacity();
        }

        // 手前の位置を返す
        inline size_type get_prev_pos(size_type pos) const
        {
            if (pos >= 1) {
                return pos - 1;
            }
            else {
                return m_size - 1;
            }
        }

        template <typename T>
        inline static auto& common_front(T* self)
        {
            assert(self->empty() == false);
            return self->m_data[self->m_front];
        }

        template <typename T>
        inline static auto& common_back(T* self)
        {
            assert(self->empty() == false);
            return self->m_data[self->get_prev_pos(self->m_back)];
        }

    private:
        container_type              m_data{};       // 要素
        size_type                   m_size{0};      // 有効な要素の個数
        size_type                   m_back{0};      // 次に push される位置
        size_type                   m_front{0};     // 次に pop される位置
    };


    // コンストラクタ(異なる要素型の RingBuffer から作成)
    template <typename T, std::size_t SIZE>
    template <typename U, std::size_t SIZE2>
    RingBuffer<T, SIZE>::RingBuffer(const RingBuffer<U, SIZE2>& other) :
        m_data(other.m_data.capacity()),
        m_size {other.m_size},
        m_back {other.m_back},
        m_front {other.m_front}
    {
        std::transform(
            std::cbegin(other.m_data),
            std::cend(other.m_data),
            std::begin(m_data),
            [](const auto& e) {
                return static_cast<T>(e);
            }
        );
    }

    // ==演算子
    template <typename T, std::size_t SIZE>
    bool RingBuffer<T, SIZE>::operator==(const RingBuffer& rhs) const
    {
        return m_data == rhs.m_data
            && m_size == rhs.m_size
            && m_back == rhs.m_back
            && m_front == rhs.m_front;
    }

    // 要素を追加
    template <typename T, std::size_t SIZE>
    void RingBuffer<T, SIZE>::push_back(const value_type& value)
    {
        if (full()) {
            m_front = get_next_pos(m_front);
        }
        else {
            m_size++;
        }

        m_data[m_back] = value;
        m_back = get_next_pos(m_back);
    }

    // 要素を取り除く
    template <typename T, std::size_t SIZE>
    void RingBuffer<T, SIZE>::pop_front()
    {
        assert(empty() == false);

        m_front = get_next_pos(m_front);
        --m_size;
    }

    // 空にする
    template <typename T, std::size_t SIZE>
    void RingBuffer<T, SIZE>::clear()
    {
        m_size = 0;
        m_back = 0;
        m_front = 0;
    }




    // ------------ Iterator ------------
    // コンストラクタ
    template <typename T, std::size_t SIZE>
    RingBuffer<T, SIZE>::Iterator::Iterator(RingBuffer& body, size_type pos, bool is_past_the_end) :
        m_body {body},
        m_pos {pos},
        m_is_past_the_end {is_past_the_end}
    {

    }

    // ++演算子(前置)
    template <typename T, std::size_t SIZE>
    typename RingBuffer<T, SIZE>::Iterator& RingBuffer<T, SIZE>::Iterator::operator++()
    {
        assert(!m_is_past_the_end);

        m_pos = m_body.get_next_pos(m_pos);

        // 終端要素の位置を越えた?
        if (m_body.get_next_pos(m_pos) == m_body.get_next_pos(m_body.end().m_pos)) {
            m_is_past_the_end = true;
        }

        return *this;
    }

    // ++演算子(後置)
    template <typename T, std::size_t SIZE>
    typename RingBuffer<T, SIZE>::Iterator RingBuffer<T, SIZE>::Iterator::operator++(int)
    {
        Iterator tmp {*this};
        ++(*this);
        return tmp;
    }


    // ------------ ConstIterator ------------
    // コンストラクタ
    template <typename T, std::size_t SIZE>
    RingBuffer<T, SIZE>::ConstIterator::ConstIterator(const RingBuffer& body, size_type pos, bool is_past_the_end) :
        m_body {body},
        m_pos {pos},
        m_is_past_the_end {is_past_the_end}
    {

    }

    // ++演算子(前置)
    template <typename T, std::size_t SIZE>
    typename RingBuffer<T, SIZE>::ConstIterator& RingBuffer<T, SIZE>::ConstIterator::operator++()
    {
        assert(!m_is_past_the_end);

        m_pos = m_body.get_next_pos(m_pos);

        // 終端要素の位置を越えた?
        if (m_body.get_next_pos(m_pos) == m_body.get_next_pos(m_body.end().m_pos)) {
            m_is_past_the_end = true;
        }

        return *this;
    }

    // ++演算子(後置)
    template <typename T, std::size_t SIZE>
    typename RingBuffer<T, SIZE>::ConstIterator RingBuffer<T, SIZE>::ConstIterator::operator++(int)
    {
        Iterator tmp {*this};
        ++(*this);
        return tmp;
    }
}

#endif
//main.cpp
#include <iostream>
#include "ring_buffer.h"

int main()
{
    mylib::RingBuffer<int, 8> rb {};

    for (int i = 0; i < 10; ++i) {
        rb.push_back(i);
    }

    const mylib::RingBuffer<int, 8> crb {rb};

    for (auto it = std::begin(rb); it != std::end(rb); ++it) {
        *it *= 10;
    }

    for (const auto& v : crb) {
        std::cout << v << "\n";
    }

    for (auto it = std::cbegin(crb); it != std::cend(crb); ++it) {
        std::cout << *it << "\n";
    }
}

実行結果:

2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9

テンプレート仮引数 SIZE を追加したほかには、これまで容量を渡していたコンストラクタは不要になるため、=default でデフォルトの実装を使うように修正しています。また、std::array は明示的に初期化子を与えてやらないと未初期化になってしまうので、m_data に宣言時の初期化子を与えておきます。

容量をコンストラクタから指定する実装と比べると、容量自体を型の一部とみなせるようになっています(前述したように、テンプレート実引数が異なれば異なる型であるため)。一方で、実行時に容量を決められる柔軟性は失われています。

また、容量が型の一部になったことで、std::queue などのコンテナアダプタの内部コンテナとして使えるようになります(「queue、priority_queue、deque」のページを参照)。内部コンテナのコンストラクタに実引数を渡す部分のコードが書けない(コンテナアダプタの実装内にある)ため、コンストラクタから容量をする方法では実現できません。

//main.cpp
#include <iostream>
#include <queue>
#include "ring_buffer.h"

int main()
{
    constexpr auto queue_size = 10;
    std::queue<int, mylib::RingBuffer<int, queue_size>> queue {};

    for (int i {0}; queue.size() < queue_size; ++i) {
        queue.push(i);
    }

    while (!queue.empty()) {
        int v {queue.front()};
        queue.pop();
        std::cout << v << "\n";
    }
}

実行結果:

0
1
2
3
4
5
6
7
8
9

std::bitset 🔗

標準ライブラリでノンタイプテンプレートが使われている例としてもう1つ、「ビット単位の処理」のページでも登場した std::bitset を挙げておきます[8]。std::bitset は、固定長のビット列を表現するクラステンプレートで、<bitset> に次のように定義されています。

namespace std {
    template <std::size_t N>
    class bitset {
        // ...
    };
}

テンプレート仮引数 N はビット数です。

std::bitset のコンストラクタは以下のように定義されています。

// 1. 空のビット列
constexpr bitset() noexcept;

// 2. 整数を指定してビット列にする
constexpr bitset(unsigned long long val) noexcept;

// 3. ビット列を表記した std::basic_string で初期化
template<class charT, class traits, class Allocator>
explicit bitset(
    const basic_string<charT,traits,Allocator>& str,
    typename basic_string<charT,traits,Allocator>::size_type pos = 0,
    typename basic_string<charT,traits,Allocator>::size_type n = basic_string<charT,traits,Allocator>::npos,
    charT zero = charT('0'),
    charT one = charT('1'));

// 4. ビット列を表記したヌル終端文字列で初期化
template <class charT>
explicit bitset(
    const charT* str,
    typename basic_string<charT>::size_type n = basic_string<charT>::npos,
    charT zero = charT('0'),
    charT one = charT('1'));

関数宣言にあらわれる constexpr や、noexcept についてはまだ解説していませんが、今のところは単純に無視しておいて問題ありません。

unsigned long long型で表現できる範囲の値なら、2つ目のコンストラクタで初期化できますが、もっと大きな値に対応するビット列を作る場合は、文字列でビット列を書き表したものを渡せる3つ目か4つ目のコンストラクタを使う必要があります。

文字列を渡すタイプのコンストラクタでは、どの文字をビット列の 01 とみなすかを指定する仮引数zero、one があります。デフォルトでは '0''1' がそれぞれ対応するようになっているので、"11011010" のような文字列を渡すと 11011010 というビット列になります。仮引数zero、one に渡すものを変えれば、"TTFTTFTF"11011010 に変換するといった使い方ができます。また、仮引数pos は文字列内の開始位置、仮引数n は文字数の指定です。

ビット列内のビットへのアクセスは [] で行います(atメンバ関数はありません)。範囲外アクセスはチェックされず、未定義動作になります。ビットを読み書きする際の型は bool ですが、変換可能なので 01 で取り扱うこともできます。

#include <bitset>
#include <iostream>

int main()
{
    constexpr auto Size = 8;
    std::bitset<Size> bs {0b11011010};

    bs[3] = false;  // bs[3] = 0; も可
    bs[4] = !bs[4];

    std::cout << bs << "\n";
}

実行結果:

11000010

<<>> による入出力が可能です。

任意の位置のビットを調べるために testメンバ関数を、任意の位置のビットを書き換えるために setメンバ関数resetメンバ関数を、ビットを反転させるために flipメンバ関数がそれぞれ定義されています。setメンバ関数は通常は true に書き換えるものですが、第2引数に false を渡して false に書き換えることができます。また、setメンバ関数、resetメンバ関数、flipメンバ関数はビット列変更後の std::bitset の参照(*this)を返します。

#include <bitset>
#include <iostream>

int main()
{
    constexpr auto Size = 8;
    std::bitset<Size> bs {0b11011010};

    bs.set(0);         // 0bit目を true に
    bs.set(1, false);  // 1bit目を false に
    bs.reset(3);       // 3bit目を false に
    bs.flip(4);        // 4bit目を反転

    for (int i {Size - 1}; i >= 0; --i) {
        std::cout << bs.test(i);  // i bit目の状態を取得する
    }
    std::cout << "\n";
}

実行結果:

11000001

setメンバ関数、resetメンバ関数、flipメンバ関数を引数なしで呼び出すと、すべてのビットに対する操作になります。

#include <bitset>
#include <iostream>

int main()
{
    constexpr auto Size = 8;
    std::bitset<Size> bs {0b11011010};

    bs.set();
    std::cout << bs << "\n";

    bs.reset();
    std::cout << bs << "\n";

    bs.flip();
    std::cout << bs << "\n";
}

実行結果:

11111111
00000000
11111111

ビット演算に関する演算子がオーバーロードされています。

#include <bitset>
#include <iostream>

int main()
{
    constexpr auto Size = 8;
    std::bitset<Size> bs {0b11011010};

    bs |= 0b00001111;
    std::cout << bs << "\n";

    bs &= 0b11110000;
    std::cout << bs << "\n";

    bs ^= 0b11111111;
    std::cout << bs << "\n";

    bs <<= 3;
    std::cout << bs << "\n";

    bs >>= 2;
    std::cout << bs << "\n";

    std::cout << (bs & std::bitset<Size>(0b00001111)) << "\n";
    std::cout << (bs | std::bitset<Size>(0b00001111)) << "\n";
    std::cout << (bs ^ std::bitset<Size>(0b00001111)) << "\n";
    std::cout << (~bs) << "\n";
}

実行結果:

11011111
11010000
00101111
01111000
00011110
00001110
00011111
00010001
11100001

==!= による比較が可能ですが、ビット数が同じでなければなりません。

#include <bitset>
#include <iostream>

int main()
{
    constexpr auto Size = 8;
    std::bitset<Size> bs1 {0b11011010};
    std::bitset<Size> bs2 {0b11111010};

    std::cout << std::boolalpha
              << (bs1 == bs2) << "\n"
              << (bs1 != bs2) << "\n";
}

実行結果:

false
true

1 になっているビットの個数を countメンバ関数で取得できます。また、すべてのビットが 1 であることを allメンバ関数で、すべてのビットが 0 であることを noneメンバ関数で、いずれかのビットが 1 であることを anyメンバ関数で調べられます。

#include <bitset>
#include <iostream>

int main()
{
    constexpr auto Size = 8;
    std::bitset<Size> bs {0b11011010};

    std::cout << "count = " << bs.count() << "\n"
              << std::boolalpha
              << "all = " << bs.all() << "\n"
              << "none = " << bs.none() << "\n"
              << "any = " << bs.any() << "\n";

    bs.reset();
    std::cout << "count = " << bs.count() << "\n"
              << std::boolalpha
              << "all = " << bs.all() << "\n"
              << "none = " << bs.none() << "\n"
              << "any = " << bs.any() << "\n";
}

実行結果:

count = 5
all = false
none = false
any = true
count = 0
all = false
none = true
any = false

to_ulongメンバ関数で unsigned long型へ、to_ullongメンバ関数で unsigned long long型へ、to_stringメンバ関数で std::basic_string に変換した値を取得できます。

#include <bitset>
#include <iostream>
#include <string>

int main()
{
    constexpr auto Size = 8;
    std::bitset<Size> bs {0b11011010};

    unsigned long ul {bs.to_ulong()};
    unsigned long long uul {bs.to_ullong()};
    std::string s {bs.to_string()};

    std::cout << ul << "\n"
              << uul << "\n"
              << s << "\n";
}

実行結果:

218
218
11011010

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次のうち、ノンタイプテンプレート仮引数の使い方に問題があるものをすべて選んでください。

  1. template <typename T, int N>
  2. template <int N1, int N2, int N3 = 0>
  3. template <int N, double F>
  4. template <char S[128]>
  5. template <std::string S>
  6. template <int* P>

解答・解説

問題2 (基本★)

size_of_array関数テンプレートの考え方を利用して、配列の末尾の要素を返す関数テンプレートを実装してください。

解答・解説

問題3 (応用★★)

std::array の要素を逆順になるように入れ替えるプログラムを作成してください。

解答・解説

問題4 (応用★★)

std::bitset の下位nビットを出力する関数テンプレートを作成してください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




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