tr1とboost/tr1でis_permutationがつらい話

なんとも重箱の隅をつつくような話だ。

私が開発に参加しているプロジェクトがあり、そこでは(古くから開発されているので)C++98が使われている。だが先進的なものも積極的に使おうとするプロジェクトでもあるので、Boostやtr1を使ってもいた。CMakeでtr1があるかどうかを確認し、存在していたらそちらをusingもしくはtypedefするという方針である。例えば、

#ifdef HAVE_CXX11_ARRAY
#include <array>
namespace hoge
{
template<typename T, std::size_t N>
struct array_getter{typedef std::array<T, N> type;};
}
#else
#include <boost/array.hpp>
namespace hoge
{
template<typename T, std::size_t N>
struct array_getter{typedef boost::array<T, N> type;};
}
#endif

のような感じだ。C++98ではテンプレートエイリアスが存在しないため、構造体を使っている。まあ、この例ではstd::arrayboost::arrayは名前が同じなのでusing std::array;でもいいと思う。

ところで、ある日私はテストを書いており、std::is_permutationが使いたくなった。そのテストでは配列持っている値は保証できるが順番は保証できないたぐいのものだったので、正解配列のpermutationであればOKだろうという判断である。今思えばstd::setに内容をコピーして正解集合と比較すればよかったのだが、その時はstd::setの存在を完全に忘れていた。

で、最初にお話しした通りそのプロジェクトではC++98が使われている。よってC++11以降で追加されたstd::is_permutationは使えない。だがC++erの強い味方Boostは使っていいことになっているので、Boost.Algorithmboost::algorithm::is_permutationが使える。

はずだった。

手元で動くことを確認した後、Travisからfailedメールが来た。調べてみると、異なる2つのstd::tr1::tupleが定義されているようだ。しかし、何故だ?

主な変更箇所はis_permutationだったので、少し覗いてみることにした。TravisではBoost 1.54が使われていたので(!)、boost.orgからダウンロードし、boost/algorithm/cxx11/is_permutation.hppを開く。

#include <boost/tr1/tr1/tuple> // for tie

とある。どうやらstd::pairに対してtieを使いたかったようだ。そのためにboost/tr1/tr1/tupleをインクルードしている。しかし、ご存知の通りboost/tr1の実装とGCCtr1実装は異なるものである。さらに、boost/tr1の内容はstd::tr1名前空間に定義される。前述した通りそのプロジェクトはtr1の機能も使っている。よって衝突が起きる。この問題の行は1.56.0時点でなくなっている。

仕方がない、と思い、その時std::setのことを思い出せば良かったのだが、何故か「is_permutationくらい自分で実装するか」と思ってしまった。本来は値の同一判定をする関数オブジェクトなどを取れるようにしてかなりジェネリックに書かないといけないのだが、今回は単にテストに使うためだけのものなので、最も簡単なものでよかろうと思い、それらの機能は無視することにした。

そして実装した。テストコードは単一の.cppファイルなので、別段名前空間を分ける必要もなかろうと思い、以下のような関数を定義した。

template<typename Iterator1, typename Iterator2>
bool is_permutation(const Iterator1 first1, const Iterator1 last1,
                    const Iterator2 first2, const Iterator2 last2);

ところが今度は手元でコンパイルが通らなかった。オーバーロード解決がambiguousだというのだ。明らかに解決できるのは私が定義した関数だけだろう、と思ったのだが、数秒経って気付いた。手元で使っているGCCは7.3で、-std=c++98などを付けなければ自動的にC++14が選択される。そして-std=c++98は使っていなかった。なのでこれと同じ形をした関数定義がstd名前空間内に存在する。

さらに、C++にはArgument Dependent Lookup (ADL)がある。これは関数の引数が何かの名前空間(例えばstd)で定義されているものなら、関数の候補をその名前空間(例えばstd)から"も"探すというものだ。この利便性と危険性に関しては星の数ほど記事があるので適宜googleしていただきたい。

で、何が起きたかというと、このis_permutationに渡していたIteratorクラスは、どちらもstd::vector<T>::iteratorだった。これは当たり前だがstd名前空間で定義されている。なので、std名前空間にある関数が全てオーバーロード解決の候補になる。GCC7.3ではデフォルトC++14なので、引数がマッチするstd::is_permutationが存在する。ところで私が定義したis_permutation関数もマッチする。よってambiguousになる。

気付くかこんなもん!!!

解決策は簡単で、自作is_permutation関数を適当な名前空間で囲み、使うときに名前空間を指定して呼べばよい。

namespace hoge
{
template<typename Iterator1, typename Iterator2>
bool is_permutation(const Iterator1 first1, const Iterator1 last1,
                    const Iterator2 first2, const Iterator2 last2);
}

hoge::is_permutation(v1.begin(), v1.end(), v2.begin(), v2.end());

あるいは、もっと単純な解決策がある。is_permutationという名前にしないことだ。そうすれば標準ライブラリと名前が被って困ることはない。


ところで、何も関係ない話だが、この前留学生に「重箱の隅をつつくってどういう意味?」と尋ねられ、「重要でない小さなことばかり議論することだ」と答えた。重箱のことは知っていたようなので、感覚もわかってもらえたようだ。すると次に、「では、豆腐の角に頭をぶつけるとは?」と聞かれ、一体どこで知ったのか非常に気になったのだが、「それは罵倒語で、馬鹿にしつつgo to hellのようなことを言っているのだ」と答えた。彼がどこでこれらの言葉を知ったのかというと、どうやら「角」と「隅」の違いについて調べていたらしい。両方vertex周辺の領域を指すが、概ね、角は出っ張っているところを言い、隅は凹んでいるところを言う、ということについて調べていた時にわからないセンテンスが出てきたので聞いてみたらしい。

最後に、「でも、”豆腐の角に頭をぶつけて死んでしまえ”は冗談だと思われるかも知れないから、罵倒語としてはあまり有用ではない。もっと端的に表現するべきだ」とも伝えておいた。これで、日本人と喧嘩になった時も、彼の怒りが正しく伝わるだろう。