[C++] ポインタを要素として持つコンテナのconst_iterator

 職場でちょっと気になることがあったので今のうちに調べておきたいと思いました。みんな大好きSTLのイテレータですが、ポインタを要素として持つコンテナのconst_iteratorの振る舞いがちょっと気になりました。

iteratorとconst_iterator

 イテレータはコンテナの要素アクセスを抽象化したもので、異なるコンテナに対して共通のアクセス手段を提供するものです。const_iteratorは参照先のデータを書き換えることができないイテレータです。イテレータそのものがconst、ではなく、コンテナが持つ要素自体がconst、という意味なのですが、このconst_iteratorは、コンテナの要素がポインタである場合に、少し直感と離れた振る舞いを示します。まずは以下のコードを見てください。

struct Hoge
{
    // 非constなメンバ関数
    void a()
    {
        std::cout << "non-const" << std::endl;
    }

    // constなメンバ関数
    void b() const
    {
        std::cout << "const" << std::endl;
    }

    Hoge() { }
};

int main()
{
    Hoge hoge;
    hoge.a(); // ok
    hoge.b(); // ok

    const Hoge const_hoge;
    const_hoge.a(); // error
    const_hoge.b(); // ok

    return 0;
}

 上記はconst修飾による挙動の違いです。const修飾されたインスタンスは書き換えが許可されませんから、constメソッド以外を呼ぶことは出来ません。次はポインタに対するconst修飾の例です(deleteは省略)。

int main()
{
    Hoge *hoge = new Hoge();
    hoge->a(); // ok
    hoge->b(); // ok

    const Hoge *const_hoge = new Hoge();
    const_hoge->a(); // error
    const_hoge->b(); // ok

    Hoge * const hoge_const = new Hoge();
    hoge_const->a(); // ok
    hoge_const->b(); // ok

    return 0;
}

 こちらも先ほどのインスタンスに対するconst修飾同様、constを付けたポインタはポインタの指し示す先の書き換えが禁止されているので、constでないメンバ関数を呼ぶことは出来ません。また、一番最後のconstの位置が「*」の後ろに付いている例ですが、これはポインタそのものの書き換えを禁止するものです。constの位置によって意味が変わるので若干覚えにくくはありますが、「*」の左側にconstがある場合はポインタが指す先が、右側にある場合はポインタそのものがconstになる、と覚えると覚えやすいと思います。「*」の左側であれば、型名の左側でも右側でも同じです。

 さて、ここでiteratorとconst_iteratorに戻りましょう。

typedef std::vector<Hoge> HogeVector;

int main()
{
    HogeVector hogeVector;
    HogeVector::iterator it1 = hogeVector.begin();
    (*it1).a(); // ok
    (*it1).b(); // ok
    
    HogeVector::const_iterator it2 = hogeVector.begin();
    (*it2).a(); // error
    (*it2).b(); // ok

    return 0;
}

 Hogeを要素にもつベクタを定義、そのiteratorとconst_iteratorにアクセスします。iteratorはconst修飾されていないのでa、b両方を呼び出せますが、const_iteratorは(要素が)const修飾された状態になるため、constメンバ関数以外にはアクセスできません。こちらの挙動は特に違和感はありません。次にポインタを要素に取ってみます。

typedef std::vector<Hoge *> HogePtrVector;

int main()
{
    HogePtrVector hogeVector;
    HogePtrVector::iterator it1 = hogeVector.begin();
    (*it1)->a(); // ok
    (*it1)->b(); // ok
    
    HogePtrVector::const_iterator it2 = hogeVector.begin();
    (*it2)->a(); // ok
    (*it2)->b(); // ok

    return 0;
}

 要素をポインタに変えただけです。iterator側はなにも問題はありません。しかし、const_iterator側は先ほどと少し結果が異なっています。const_iteratorを使っているにもかかわらず、constメンバ関数の呼び出しが出来てしまっています。

constはどこに付く?

 さて、どうしてconst_iteratorにもかかわらず、constメンバ関数以外にアクセスできてしまったのでしょうか?結論から言えば、const修飾されるのは、その要素ではなくiteratorの中身だからです。複雑ですね。コンテナの要素がconst修飾されているわけでも、イテレータそのものがconst修飾されているわけでもなく、イテレータ内部にある、要素へのポインタがconst修飾されます。

int main()
{
    HogePtrVector hogeVector;
    HogePtrVector::iterator it1 = hogeVector.begin();
    it1 = hogeVector.begin(); // ok
    *it1 = nullptr; // ok
    
    HogePtrVector::const_iterator it2 = hogeVector.begin();
    it2 = hogeVector.begin(); // ok
    *it2 = nullptr; // error

    return 0;
}

 上記のコードより、どちらもイテレータそのものへの代入は可能ですが、const_iteratorはイテレータの指し示す先への代入が出来ていません。つまり、*it2は「Hoge const *」である、ということになります(もちろん、STLのコードをみてもそのように定義してあります)。「const Hoge *」ではありません。違和感の正体はここで、今まで要素そのものがconst修飾されると勘違いしていました。

 では、どうしてポインタではなく実体を要素に持つと、あたかも要素がconst修飾されたように振る舞うのでしょうか。以下のコードを見てみましょう。

int main()
{
    Hoge const const_hoge;
    const_hoge.a(); // error
    const_hoge.b(); // ok

    return 0;
}

 むむ、constの位置をずらしても結果はおなじになりました。先ほど述べましたが、constは型名のどちら側に来ても意味は同じです。したがって、「Hoge const」も「const Hoge」も同じであり、変数そのものがconst修飾されたのと同義となり、const修飾同様に振る舞うようになります。

ポインタの場合と実体の場合の違い

 さて、ここで少し話を整理してみましょう。今回の話題はconst_iteratorの振る舞いに関してですが、どうやらconstの付く位置に関係しているようだ、ということが分かりました。ポインタ、実体それぞれconstの付く位置は以下のとおりです。

    const Hoge hoge;
    Hoge const hoge; // 上と同じ意味…(A)
    const Hoge *hoge;
    Hoge const *hoge // 上と同じ意味
    Hoge * const hoge; // 変数自体の書き換えができない…(B)

 なにか見えてきましたね。見やすくするために、(A)と(B)をカッコで括って比べてみます。

    (Hoge) const hoge; // hogeがconst修飾される
    (Hoge *) const hoge; // 変数自体の書き換えができない

 カッコで括られた部分がちょうどvectorの要素の型名になっています。振る舞いの違いはこの文法の微妙な差から来ていたのですね。

なぜ微妙に違うのか

 では、なぜ実体とポインタでconstの意味が微妙に異なってくるのでしょうか。それは、実体の性質とポインタの性質の違いが関係しています。当然ですが、ポインタは実体ではありません。あくまで実体のある場所を指しているものに過ぎません。つまり、実体の場合はその変数に何かを代入する行為そのものが実体を操作する行為であるのに対し、ポインタの場合は実体ではなく実体を示すものを操作する、という違いがあるということです。このことから、実体への代入を禁止するということはすなわち実体そのものの操作を禁止する、ということであり、ポインタへの代入を禁止してもポインタが指し示す先への変更は可能である、ということがわかると思います。

 具体例として、ある型のポインタ型を一つの型としてみなす(ある型に対するポインタ、ではなく、ある型のポインタの実体と考える)なら、実体の時と同じ表現になります。

    // hogeの書き換えは可能(hogeの指す先は書き換え不可能)
    (const Hoge *) hoge;
    // hogeの書き換えは不可能(hogeの指す先は書き換え可能)
    (Hoge *) const hoge;

書き換えを禁止するには

 コンテナの要素の書き換えを禁止するには、

// テンプレート引数自体をconstにする
typedef std::vector<const Hoge *> HogePtrVector;

 こうなります。こうすれば、const_iteratorの中身も

    (const Hoge *) const

になり、一切書き換えのできないイテレータとなります。また、よく

typedef std::vector<const Hoge> HogeVector;

のようなコードを見かけますが、おそらく意図した意味にならないことも今回の説明でわかっていただけたかと思います。

まとめ

 「調べる」と言いましたが記事を書いているうちに自己解決してました。個人的にはC++の闇の1つがこのconstだと思っています。感染力が強く、慣れないうちは扱いにくいconstですが、慣れるとconst無しではコードが書けなくなるばかりか、いつしかコードの中にconstを求めるようになり、constの無いコードを読むだけで吐き気、めまい、動悸、息切れ、食欲不振、人間不信等の症状を引き起こし、最悪の場合は死に至るようになります。しかし、constのある人生はそれのない人生と比較しても充実したものとなり、死後も絶対神constの力により救いがあると言われています。const、最高!

 すみません、我を忘れていました。iteratiorとconst_iteratorの実体とポインタに対する振る舞いの違い、理解していただけたでしょうか。C++はとても難しい言語ですが、少しずつ分解しながら要素要素を確かめていけば理解も深まると思います。少しずつ理解を深め、楽しい(闇の)C++ライフを送りましょう!

0 件のコメント :

コメントを投稿