C++言語の持つオシャレな機構の一つにADL(Argument Dependent Lookup)というものがあります。
和訳すると「実引数依存の名前検索」となり、読んで字の如く、「実引数」に依存した「名前検索」となります。なるほどわからん。
ADLの挙動
コードを見ながら順を追って理解してみましょう。
|
int main() { func(); return 0; } |
funcという関数が呼ばれるコードですが、これをただコンパイルするとエラーになります。
なぜならば……funcという関数は見る限りどこにも定義されていないからです!!
…………。
では、定義しましょう。
|
#include <iostream> void func() { std::cout << "call func. yes!" << std::endl; } int main() { func(); return 0; } |
今度はコンパイルエラーにならずに無事、func関数が呼び出せます。
当たり前すぎて、今、自分が何を読んでいるのか分からなくなってきたのではないでしょうか。明日はどっちでしょうか。
先へ進みます。
|
#include <iostream> namespace ns1 { void func() { std::cout << "call func. yes!" << std::endl; } } // namespace ns1 int main() { func(); return 0; } |
func関数をnamespace ns1で囲みました。
このコードは再度コンパイルエラーになります。当たり前ですね。
9行目で呼ばれているfuncのシンボルをグローバルネームスペースから探そうとするのですが、実際に定義されているfuncはns1ネームスペースの中にある為、通常の名前検索ではヒットしない為です。
なのでネームスペースを指定した上で呼び出す必要があります。
|
#include <iostream> namespace ns1 { void func() { std::cout << "call func. yes!" << std::endl; } } // namespace ns1 int main() { ns1::func(); // ns1のfuncを呼び出す return 0; } |
まったく何を読まされているんだという感じでしょうが、ここまではウォーミングアップです。おつかれさまでした。
ではもっと具体的な実装コードを見てみましょう。
namespace ns1に色情報を持つ構造体と、内容を出力する関数を定義しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
#include <iostream> namespace ns1 { struct Color { int r = 255; // ADLとは関係無いが、c++11から int g = 255; // 非staticなデータメンバの宣言時に int b = 0; // 初期値の指定ができるようになったよ }; void print(Color& color) { std::cout << color.r << std::endl; std::cout << color.g << std::endl; std::cout << color.b << std::endl; } } // namespace ns1 int main() { ns1::Color c; print(c); // printにns1を付けずに呼ぶ。※タイプミスではない return 0; } |
このmain関数をよく見て下さい。
Colorの型にはns1::という指定が付いていますが、printはネームスペースの指定を付けずに呼び出しています。
しかし、このコードはコンパイルエラーになりません。
なぜならc++様はある型のオブジェクトが関数呼出の際に実引数として用いられると、関連するネームスペースからも、その関数が探索されるという仕様だからです。
今回の場合は、printの引数で渡されているcの型がns1::Colorで、ns1ネームスペースに関係があるため、printという関数のシンボル検索グローバルネームスペースからだけではなく、関係があるns1からも検索するという挙動になります。
これが「実引数依存の名前検索(ADL)」の挙動です。
では「関連するネームスペース」とは何でしょうか。
引数がクラスのメンバだった場合
そのクラス自身および基底クラス、そしてそのクラスを囲むネームスペースです。
例えばstd::vector::iteratorはクラスのメンバですので、vectorとstdが関連するネームスペースになりますね。
引数がネームスペースのメンバだった場合
あるネームスペースの中で定義されているだけのクラスの場合、関連するネームスペースはそのクラスを囲むネームスペースです。そのクラスが継承していれば継承元のクラスに対して、同様の条件に基いて関連するネームスペースが決定します。
注意点
前述のとおりADLは人間の目では一見しただけではわからないネームスペースからもシンボルを探そうとします。
その結果、本来意図しない関数が呼び出される危険性もあります。
でも大丈夫、予期せぬADLの防止策もちゃんとあります。
ADL Firewall
また、この辺りの注意点について詳しく解説しているサイトも多いので色々参照してみるとよいでしょう。
google検索[ADL c++ 注意]
なぜ存在するのか
複雑性を増すだけに思えるこのADLという仕組みがなぜ必要なのか。それは演算子をネームスペースの修飾無しで呼べるようにするためです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
#include <cstdio> namespace ns2 { struct Money { Money(int v) : value(v) {} int value; }; // Money用の2項+演算子 Money operator + (const Money& lhs, const Money&rhs) { return Money(lhs.value + rhs.value); } } // namespace ns2 int main() { ns2::Money a(5), b(1); ns2::Money c = a + b; // もしADLがなかったら2項演算子+がこう呼べない return 0; } |
ADLという仕組みがないと
の部分を
|
ns2::Money c = ns2::operator+(a, b); |
と、何とも味わい深い呼び出し方をするしかなくなってしまいます。
これはストリーム演算子含むあらゆる演算子に言えることです。
よく登場する<<演算子も本来stdのネームスペースの中にあるので、ADLが無いと直感的に呼び出せないのです。
|
#include <iostream> #include <string> int main() { std::string one_more_thing = "8K iMac coming soon"; std::cout << one_more_thing << std::endl; // std::<<の呼び出し } |
またSTLのswapのように(と言っても他に良い例を知らないですが)あえてADLの挙動を利用して汎用的な処理ができるようにしているコードもあります。
STLの中で呼ばれる修飾無しのswapはユーザー定義のものがADLで見つかればそれを呼ぶし無ければstd::swapが呼ばれるという実装になっています。
まとめ
- ADLの挙動を理解しよう
- ADLによる名前解決の結果、実行するまでバグと分からないことがある
- ADLのお陰で演算子が完結に書けるというメリットもある
- 世の中にはADLの挙動を意図的に利用した悪魔的なコードもある