C++ ADLとは|問題と危険性、回避方法(明示的回避と事前回避)

本記事ではADLの概要を説明します。またADLの利点や問題点、危険性、ADLの回避方法についても詳細に解説していきます。

目次

ADLとは

ADL(argument dependent lookup, 実引数依存の名前探索)は実引数の型を活用した名前解決の特別なルールです。ADLは関数呼び出し時の実引数の型を基準に関数名を探索します。そしてその探索の範囲は、その実引数が属する名前空間にまで及ぶ点が最大の特徴です。

namespace ns {
   class X {};
   void f(X) {}
}

int main() {
   f(ns::X{}); // `ns::f(X)`関数が呼ばれる
}

上記main関数内で呼び出しているf関数は、一見するとグローバル空間のf関数を呼び出しているように見えますが、実際には名前空間ns内部のf関数を呼び出します。

f関数の名前探索は実引数型Xが属する名前空間nsを基準に行われるため、このような呼び出しが行われるのです。そしてこれを実現する仕組みがADLというわけです。

ちなみに、ADLは「argument dependent name lookup」や「Koenig lookup」と呼ばれることもあります。

ADLの利点と活用例

ADLの活用によって面倒な名前空間の明示を省略できるようになるという利点が生まれます。実際に、ADLはC++の標準入出力ストリームの<<演算子にも活用されています。

std::cout << std::string("hello");

上記の<<演算子は実は名前空間stdの内部で定義されています。ADLの仕組みが無いと、以下のように名前空間を明示した形で演算子を呼び出さなければならなくなります。

std::operator<<(std::cout, std::string("hello"));

しかしADLのルールがあれば、実引数std::coutの型を基準に、名前空間stdからstd::operator<<(ostream&, const string&)演算子関数の定義を探索することが可能になるため、先程の面倒な名前空間の明示も不要となります。

ADLの発動パターン

テンプレート関数への探索

ADLは一定の条件を満たせば、C++標準ライブラリのテンプレート関数に対しても働くため、以下のような記述も可能となります。

std::vector<int> v = {1, 2, 3};
move(v); // std::move(v)に名前解決される
for_each(begin(v), end(v), [](auto w) {});

利用される実引数型と同一の名前空間で宣言されたテンプレート関数は、ADLによる探索の対象となるため、このようなstdの省略が可能となっているのです。

ただし、多重定義時やユーザ定義関数との混在時に余計な混乱が発生することがあるため、このような省略の多用はオススメできません。詳しい理由は次の節で説明します。

なお、begin関数やend関数へのADLはstd以外の名前空間で定義された基本型に対しては働かない点に注意が必要です。

int a[] = {1, 2, 3};
begin(a); // ERROR: Use of undeclared identifier 'begin'; did you mean 'std::begin'?

この場合は名前空間stdを明示したり、using宣言を用いる必要があります。

int a[] = {1, 2, 3};

std::begin(a); // OK

using std::begin;
begin(a);      // OK

サードパーティ製のライブラリでusing std::begin;using std::end;という記法が用いられている事がありますが、あれはまさに配列型へのADLが効かない問題に対する対処として用いられているのです。ちなみにusingディレクティブを用いても、基本はまずADLが優先されます。

基底クラスの名前空間に対する探索

ADLによる探索は基底クラスが宣言された名前空間にまで及びます。

int main() {
   struct A : std::string {};
   move(A{}); // OK: std::moveが呼ばれる
   
   struct B {};
   move(B{}); // ERROR: Use of undeclared identifier 'move'; did you mean 'std::move'?
}

このように、派生クラスが基底クラスとは異なる名前空間で宣言されているにもかかわらず、基底クラス依存のADLが発生してしまっていることが分かります。

テンプレート引数による探索

テンプレート引数として指定された型も探索の対象となります。

template<class T> struct Y {};

int main() {
   move(Y<std::string>{}); // OK: std::moveが呼ばれる
   move(Y<int>{});         // ERROR: Use of undeclared identifier 'move'
}

テンプレート仮引数による探索

ADLはテンプレート関数の仮引数型に対しても働きます。

template<class T> void g(T v) { move(v); }

int main() {
   g(std::string{}); // OK: テンプレート関数側でstd::moveが呼ばれる
   g(99);            // ERROR: Use of undeclared identifier 'move'
}

なお標準ライブラリやサードパーティ製のライブラリでは、このような仮引数に対するADLの発動を意図的に活用する場合もあります。ユーザ側で多重定義された関数(begin/end関数やswap関数等)を有効化するためです。

ADLの問題と危険性

ADLは名前空間の面倒な明示を省略できる点が便利ですが、ユーザ定義型との混在時や関数の多重定義時には余計な混乱や問題を引き起こすことがあります。

namespace ns {
   class X {};
}

template<class T> void f(T t) { puts("f(T)"); }

int main() {
   f(ns::X{}); // "f(T)"
}

上記の例ではグローバル空間のf関数が呼ばれます。特に問題はない普通のコードです。

ただ、名前空間nsの側で同名のf関数を定義してしまうと、main関数側ではADLによってns空間側のf関数が呼ばれるようになってしまいます。

namespace ns {
   class X {}; // ↓ 後付けで定義
   void f(X x) { puts("ns::f(X)"); }
}

template<class T> void f(T t) { puts("f(T)"); }

int main() {
   f(ns::X{}); // "ns::f(X)"
   // f(T)が呼ばれなくなってしまった
}

これは大変な問題です。名前空間ns内に関数を追加しただけで、元のコードが今までとは違う動作に変ってしまうわけですから。

上記のような挙動の変化が、意図的な物であればまったく問題は無いですが、多くの開発シーンでは、上記のような変化が知らぬ間に、気づかない内に引き起こってしまう事があります。それはまさにADLの危険性であり、ADLの罠や落とし穴とも言うべきものです。

ADLの回避(明示的回避)

ADLの問題や危険性を回避するためには、ADLに依存しないコードを書く必要があります。

名前空間の明示

名前空間を明示することでADLの発動を回避することができます。以下のコードでは名前空間stdを明示的に修飾することで、C++標準ライブラリ側のmove関数を明示的に呼び出しています。

namespace ns {
   class X {};
   X move(X x) { return x; }
}

int main() {
        move(ns::X{}); //  ns::move(X)が呼ばれてしまう
   std::move(ns::X{}); // std::move(T)が呼ばれる
}

グローバル関数の明示

グローバル関数の場合はスコープ解決演算子::のスコープ省略記法を用いることで、グローバル関数を明示的に呼び出すことができます。

namespace ns {
   class X {};
   void f(X x) {}
}

template<class T> void f(T t) {}

int main() {
     f(ns::X{}); // ns::f(X)が呼ばれてしまう
   ::f(ns::X{}); //     f(T)が呼ばれる
}

丸括弧によるADLの回避

関数名を括弧で囲うことで、ADLの発動を回避することができます。

namespace ns {
   class X {};
   void f(X x) {}
}

template<class T> void f(T t) {}

int main() {
     f(ns::X{}); // ns::f(X)が呼ばれてしまう
   (f)(ns::X{}); //     f(T)が呼ばれる
}

ADLの回避(事前回避)

ADL回避対象の関数をダミーの名前空間でラップすることで、ADLを回避することが可能です。ラップされた関数はusingディレクティブで本来の名前空間に展開します。

以下の例は、namespace aがADL対策のされていない名前空間で、namespace bがADL対策後の名前空間です。関数呼び出しf(b)では、グローバル空間側の関数が呼び出されていることがわかります。

namespace a { // ADL対策してない
   class X {};
   template<class T> void f(T&) {}
}

namespace b { // ADL対策した
   class X {};
   namespace avoid_adl { // 待避対象の関数をラップする
      template<class T> void f(T&) {}
   }
   using namespace avoid_adl; // 名前空間`b`に展開し直す
}

template<class T> void f(const T&) {}

void test() {
   a::X a;
   f(a);   // a::f(T&)が呼ばれてしまう
   b::X b;
   f(b);   // f(const T&)が呼ばれる
}

ちなみに、無名名前空間でADLを回避することもできます。

namespace c {
   class X {};
   namespace {
      template<class T> void f(T&) {}
   }
}

template<class T> void f(const T&) {}

void test() {
   c::X c;
   f(c);  // f(const T&)が呼ばれる
}
広告