What is it, naokirin?

C++のADL(Argument-dependent name lookup)

昨日、宮城県沖を震源とする地震が発生していましたね。地震だけでなく津波による被害も大きいようですね。被害に合われた皆さんが一日も早く平穏な生活を取り戻せることを願っています。

ちなみに私は生まれも育ちも現住所も西日本なので、全く被害はありません。ご心配なく。


さて、今回はC++のADL(Argument-dependent name lookup)について軽く書いてみます。
とは言っても、C++をそこそこ使っている人なら、規格の最初の方に書いてあるようなごく当たり前のことかもしれませんね。

日本語で表すと「引数による名前の探索」と言ったところでしょうか。引数の所属する空間から関数を探索するわけです。

今回は例としてswap()という関数を作って、どのように動くかを見てみます。(あまりここまで汎用的なswap()を自分で定義する必要は無いようにも思えますが…)

#include <vector>
#include <iostream>

namespace Samples
{
  template <class T>
  void swap(T &x, T &y)
  {
    T z = x;
    x = y;
    y = z;
    std::cout << "Samples::swap() done." << std::endl;
  }  

  class Sample
  {
  public:
    Sample(int i)
    {
      p = i;
    }
    int p;
  };
};


int main()
{
  Samples::Sample s(1), t(2);
  swap(s, t);  // Samples::swap()関数が呼ばれる

  std::cout << "swap() for Samples::Sample is done." << std::endl;

  std::vector<int> v;
  std::vector<int> w;

  v.push_back(1);
  w.push_back(2);

  swap(v, w);  // (vectorのヘッダで定義される)std::swap()が呼ばれる

  std::cout << "swap() for std::vector is done." << std::endl;

  return 0;
}

実際にこのコードを実行すると

Samples::swap() is done.
swap() for Samples::Sample is done.
swap() for std::vector is done.

のように動作します。つまり、swap()が引数にSamples::Sample型の変数を渡した時はSamples名前空間で定義したswap()関数、引数にstd::vector型の変数を渡した時はこのコードのものではないswap()関数(std空間内のswap()関数)が呼ばれています。

ちなみに、次のような場合

using std::swap;
swap(s, t);

と上記のコード内で呼びだした場合、呼び出されるswap()関数が自分で作ったswap()か、std空間内のswap()かはC++03では定義されていないようです。std空間のswap()関数を明示的に呼び出したい場合、

std::swap(s, t);

と呼び出すとちゃんとstd空間内のswap()関数を呼び出してくれます。

こうやってみてみると、C++を使っている人なら意外と身近でもADLは起こってそうです。じつは上のサンプルコードには身近なADLのサンプルが隠れています。

std::cout << "" << std::endl;

のようなコードを書いていますが、using namespace std; をしていない限り通常(ADLが働かない状況なら)、「std::」のように関数はスコープを指定していなければいけません。しかしよく考えてみれば「<<」も元々は

std::ostream& std::operator <<(std::ostream&, const std::string&)

のような関数のはずです。これが「std::」をつけずに呼び出されるのはADLのおかげというわけです。

もちろんADLにはいいことばかりでなく、よく余計な探索を行ってしまうといったことがあります。つまり意図しない関数を呼び出して、ユーザを混乱させるわけです。
その辺りは一部の有名なブログでも紹介されていますが、先ほど見たように名前空間の宣言無しでも名前空間内の関数を呼び出してしまいます。
「たまたま外部のライブラリ内に定義されていた関数が呼び出されてしまって謎の挙動をしてしまっていた」ということも考えられます。(グローバルスコープでswap()を自分で定義していても、引数にstd名前空間内で定義された型のデータを渡していると、std::swap()が呼ばれてしまいます。std::swap()を知らない人であれば混乱するでしょう。)

ADLについてはC++0xでさらに細かい動作定義がおこなわれる模様なので、そちらでさらに仕様が確定すると混乱も起きにくいかもしれません。ちなみに今回は取り上げませんでしたが、ADLの回避方法として梶本裕介の日記に紹介があります。