C++イテレーターの仕組みと使い方

C++イテレーターの仕組みと使い方

イテレーターが最も便利なのはアルゴリズムライブラリとの連携です。この仕組みを活用することで、アルゴリズムの実装に焦点を当てることができ、データ構造の詳細を気にする必要がありません。

イテレーターの基本的な使い方

例えば、std::copy_nは範囲内の要素をコピーするためのアルゴリズムです。この関数はさまざまなコンテナに対応しており、back_inserterfront_inserterなどの上位イテレーターを使用することも可能です。


// std::vectorのコピー
std::vector<int> a1{2, 1, 3, 4};

std::vector<int> a2(a1.size() / 2);                 // 初期サイズは2
std::copy_n(a1.cbegin(), a1.size() / 2, a2.begin()); // a2には2,1がコピーされる

std::vector<int> a3;                                             // 初期サイズ0
std::copy_n(a1.cbegin(), a1.size() / 2, std::back_inserter(a3)); // a3には2,1がコピーされる

// std::list
std::list<int> lst1{12, 11, 13, 14};
std::list<int> lst2;
std::copy_n(lst1.cbegin(), lst1.size() / 2, std::back_inserter(lst2)); // lst2には12,11がコピーされる

// std::vector -> std::list
std::list<int> lst3;
std::copy_n(a1.cbegin(), a1.size() / 2, std::back_inserter(lst3)); // lst3には2,1がコピーされる

// std::list -> std::vector
std::vector<int> a4;
std::copy_n(lst1.cbegin(), lst1.size() / 2, std::back_inserter(a4)); // a4には12,11がコピーされる

要素のコピーは単純なmemcpyではなく、深コピーの可能性もあります。他の言語(例えばGo)では、標準ライブラリのアルゴリズムが限られているため、このような統一されたインターフェースがありません。

イテレーターの設計

STLのstd::copy関数の実装(GCC12の例)を見ると、イテレーターの設計思想が分かります。


template <bool _IsMove, bool _IsSimple, typename _Category>
struct __copy_move {
    template <typename _II, typename _OI>
    _GLIBCXX20_CONSTEXPR
    static _OI __copy_m(_II __first, _II __last, _OI __result) {
        for (; __first != __last; ++__result, (void)++__first)
            *__result = *__first;
        return __result;
    }
};

この実装で使用されている演算子は次の通りです。

  • 同値演算子:`!=`、`==`、`<`、`<=`、`>`、`>=`
  • インクリメント/デクリメント:`++`、`--`、`++(int)`、`--(int)`
  • 要素へのアクセス:`*`、`->`

イテレーターは基本的にクラスとして設計されます。コンテナごとに異なるイテレーターを定義し、iterator_traitsを介して共通のインターフェースを提供します。

コンテナ固有のイテレーター

各コンテナのイテレーターは以下のようになっています。

  • std::vector:`__normal_iterator`
  • std::list:`_List_iterator`
  • std::map:`_Rb_tree_iterator`

例えば、std::vectorのイテレーターは指针をラップしています。


reference operator*() const noexcept { return *_M_current; }

__normal_iterator &operator++() noexcept {
    ++_M_current;
    return *this;
}

std::listのイテレーターはリンクリストのノードを指针として管理しています。


reference operator*() const noexcept { return *static_cast<_Node *>(_M_node)->_M_valptr(); }

_Self &operator++() noexcept {
    _M_node = _M_node->_M_next;
    return *this;
}

std::mapのイテレーターは赤黒木のノードを指针として管理しています。


reference operator*() const noexcept {
    return *static_cast<_Link_type>(_M_node)->_M_valptr();
}

_Self &operator++() noexcept {
    _M_node = _Rb_tree_increment(_M_node);
    return *this;
}

イテレーターの実装と特徴

iterator_traitsはイテレーターの特性を定義します。例えば、std::vectorのイテレーターは次の特性を持ちます。


template <typename _Iterator>
struct __iterator_traits<
    _Iterator, __void_t<typename _Iterator::iterator_category,
                        typename _Iterator::value_type,
                        typename _Iterator::difference_type,
                        typename _Iterator::pointer,
                        typename _Iterator::reference>> {
    typedef typename _Iterator::iterator_category iterator_category;
    typedef typename _Iterator::value_type value_type;
    typedef typename _Iterator::difference_type difference_type;
    typedef typename _Iterator::pointer pointer;
    typedef typename _Iterator::reference reference;
};

template <typename _Iterator>
struct iterator_traits : public __iterator_traits<_Iterator> {};

この仕組みは、std::vectorstd::mapなどのコンテナのイテレーターを統一的に扱うことを可能にします。

イテレーターの種類

イテレーターは大きく以下の種類に分かれます。

  • reverse_iterator:コンテナを逆順にイテレートします。
  • back_inserter:コンテナの末尾に要素を追加します。
  • front_inserter:コンテナの先頭に要素を追加します。
  • inserter:指定された位置に要素を挿入します。

イテレーターの有効性問題

イテレーターが無効になる場合として、コンテナの要素を削除する場合があります。

std::vectorの削除操作は以下のように実装されています。


template <typename _Alloc>
typename vector::iterator vector::_M_erase(
    iterator __position) {
    if (__position + 1 != end())
        std::copy(__position + 1, end(), __position);
    --this->_M_impl._M_finish;
    return __position;
}

std::listの削除操作は以下のように実装されています。


void _M_erase(iterator __position) noexcept {
    this->_M_dec_size(1);
    __position._M_node->_M_unhook();
    _Node *__n = static_cast<_Node *>(__position._M_node);
    _Node_alloc_traits::destroy(_M_get_Node_allocator(), __n->_M_valptr());
    _M_put_node(__n);
}

template <typename _Tp, typename _Alloc>
typename list<_Tp, _Alloc>::iterator list<_Tp, _Alloc>::erase(const_iterator __position) noexcept {
    iterator __ret = iterator(__position._M_node->_M_next);
    _M_erase(__position._M_const_cast());
    return __ret;
}

std::mapの削除操作は以下のように実装されています。


iterator erase(iterator __position) {
    do {
        if (std::__is_constant_evaluated() &&
            !bool(__position != end()))
            __builtin_unreachable();
    } while (false);
    iterator __result = __position;
    ++__result;
    _M_erase_aux(__position);
    return __result;
}

削除後のイテレーターの有効性はコンテナの種類によって異なります。

  • std::vector:要素削除後にイテレーターが指针する位置が変化します。
  • std::list:削除された要素の指针は無効になります。
  • std::map:削除された要素の指针は無効になります。

削除後のイテレーターを安全に使用するには、削除後の状態を管理する必要があります。例えば、std::mapの削除後のイテレーターは以下のように管理します。


std::map m1{{"lebron", 6}, {"tom", 2}, {"haha", 3}};
for (auto it = m1.begin(); it != m1.end();) {
    // 削除後のイテレーターを管理
    it = m1.erase(it);
}

以上の仕組みを活用することで、STLのアルゴリズムを柔軟に使用することができます。

タグ: C++ イテレーター STL コピー アルゴリズム

8月24日 21:11 投稿