gcc 4.8.xでの標準ライブラリのバグについて

結構昔に書いていたっぽい下書きがあったので手直しして出しておこう。まあイマドキgcc-4.8.5を使う羽目になっている人はあまりいないだろうが、少なくともIssueが立つ程度には存在するようなので。

広く知られていることっぽいが、GCC 4.8系統の標準ライブラリ実装にはバグがある(他にもいろんなバグがあるが)。古いパッケージを使い続けることを特徴としている一部のLinuxフレーバーでは未だにGCCが 4.8.5だったりするので注意が必要かもしれない。数ヶ月前にIntelコンパイラでビルドするとtoml11が落ちるというIssueが立ったが、手元のicpcでは落ちなかったので、これは環境に入っている標準ライブラリ側の問題だと直感し、GCC 4.8.5で試した所再現したという経緯で発見した。

github.com

この問題を紹介しておこうと思う。

背景

バグの内容だけ知りたい人は飛ばしてください。

前提として、toml11ではTOML上での値に対するcommentを持ったり持たなかったりするために、std::vector<std::string>をラップした構造体、discard_commentspreserve_commentsをテンプレートパラメータで受け取る。双方std::vector<std::string>と同様のインターフェースを持っているが、discard_commentsは何をしても常に空で、preserve_commentsは普通のvectorのように振る舞う。

すると、comments()でコメントにアクセスできるというインターフェースは変わらず、コメントを持たないことを選択した場合はコメント追加が無視され、コメント一覧は常に空ということになる。

なんでそんなややこしいことになってしまったかというと、まず第一にはテンプレートを使えばコメントがいらない人は無意味なvectorを持たなくて済むというのがあった。これはまあ仕方ない。続いて、初期にはリードオンリーのコメントアクセスを常に許していたということがあり(これは話が長くなるので別の機会になんでそうなったか書きたいと思う)、そのインターフェースを変更するのはどうかという気持ちがあった。変更しても良かったんじゃないかとは今は少し思っている。あとパーサ側の実装の単純さが大きい。パラメータによらずとにかくコメントをpush_backしておくと、discard_commentsは勝手にコメントを無視してくれるので、コメントを保存するかどうかのifブロックが必要ない。

バグの内容

というわけで、preserve_commentsは中で持っているstd::vectorのインターフェースをそのまま外にリダイレクトしている。当然、inserteraseがある。C++11からstd::vector::insertconst_iteratorを受け取るようになった。

    iterator insert(const_iterator p, const std::string& x)
    {
        return comments.insert(p, x);
    }

だが、gcc 4.8.x系のstd::vector実装はiteratorを受け取るままになっていた。constな参照やポインタは非constな参照やポインタに変換できない。書き換えられないはずの参照を書き換え可能な参照に簡単に変換できてしまったら意味がないからだ。なので、上の関数はgcc 4.8.xではコンパイルに失敗する。const_iteratoriteratorに変換しようとしているので。

std::vectorのインターフェースは標準規格で定まっているので、この場合誰が間違えたかははっきりしている。gcc 4.8.xの標準ライブラリ実装が型を変え忘れたのだ。同じことがeraseやあろうことかC++11で追加されたはずのemplaceでも起きている。ということは型の部分はコピペしたんだろうか。GCCみたいな誰もが知ってる巨大プロジェクトでもこういうことが起きるのはちょっとほっこりしますね。いやまあホッコリしてる場合ではないが。

gcc 4.8.xのためだけにこの関数がiteratorを取るように変えるわけにはいかない。そうするとユーザーがconst_iteratorを渡せなくなってしまう。他のコンパイラを使っていてstd::vectorのインターフェースを知っている人を困惑させることになる。それに、そもそもコンパイラがミスってるのにこっちが追従する必要はない。

ワークアラウンド

なんか華麗なワークアラウンドがあるとかではないです。ゴリ押し。

以下はtoml11の該当箇所をコピペしたもの。

    // It is known that `std::vector::insert` and `std::vector::erase` in
    // the standard library implementation included in GCC 4.8.5 takes
    // `std::vector::iterator` instead of `std::vector::const_iterator`.
    // Because of the const-correctness, we cannot convert a `const_iterator` to
    // an `iterator`. It causes compilation error in GCC 4.8.5.
#if defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__)
#  if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) <= 40805
#    define TOML11_WORKAROUND_GCC_4_8_X_STANDARD_LIBRARY_IMPLEMENTATION
#  endif
#endif

これでGCC 4.8.x以前の場合だけこのマクロが定義される。絶対にミスで使わないようにこの文脈で考え得る限りもっとも長い名前をつけた。

あとはこれを#ifdefで使ってウオー!ってやって終わり。

結論

頼むからシステムのコンパイラをアップデートしてくれ。

そもそもなんで新しめのコンパイラを使わないという選択肢がありえるのかよくわかってない。OSとかだと無限にテストケースがあって全部やり直すのはだるいとかなんだろうか。