なんとも重箱の隅をつつくような話だ。
私が開発に参加しているプロジェクトがあり、そこでは(古くから開発されているので)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::array
とboost::array
は名前が同じなのでusing std::array;
でもいいと思う。
ところで、ある日私はテストを書いており、std::is_permutation
が使いたくなった。そのテストでは配列持っている値は保証できるが順番は保証できないたぐいのものだったので、正解配列のpermutation
であればOKだろうという判断である。今思えばstd::set
に内容をコピーして正解集合と比較すればよかったのだが、その時はstd::set
の存在を完全に忘れていた。
で、最初にお話しした通りそのプロジェクトではC++98が使われている。よってC++11以降で追加されたstd::is_permutation
は使えない。だがC++erの強い味方Boostは使っていいことになっているので、Boost.Algorithm
のboost::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
の実装とGCCのtr1
実装は異なるものである。さらに、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周辺の領域を指すが、概ね、角は出っ張っているところを言い、隅は凹んでいるところを言う、ということについて調べていた時にわからないセンテンスが出てきたので聞いてみたらしい。
最後に、「でも、”豆腐の角に頭をぶつけて死んでしまえ”は冗談だと思われるかも知れないから、罵倒語としてはあまり有用ではない。もっと端的に表現するべきだ」とも伝えておいた。これで、日本人と喧嘩になった時も、彼の怒りが正しく伝わるだろう。