C++ の名前解決
はじめに
標準を読むと C++ の名前解決の決まりについて細かく規定されているのがわかると思いますが、ここではその詳細な検索の順番や個別規則の詳細などについては説明しません
ここでは直観的でない2つの決まりについて説明します
一つは一般の名前解決での「Argument-dependent name lookup」という決まり、もう一つは、テンプレートに関係する名前解決での「Two phase name lookup」という決まりです
標準では
標準では次の辺りに詳しく書いてあります
一般の名前解決
3 Basic concepts 3.4 Name lookup 3.4.1 Unqualified name lookup 3.4.2 Argument-dependent name lookup 3.4.3 Qualified name lookup 3.4.3.1 Class members 3.4.3.2 Namespace members 3.4.4 Elaborated type specifiers 3.4.5 Class member access 3.4.6 Using-directives and namespace aliases
テンプレートに関係する名前解決
14 Templates 14.6 Name resolution 14.6.2 Dependent names 14.6.2.1 Dependent types 14.6.2.2 Type-dependent expressions 14.6.2.3 Value-dependent expressions 14.6.2.4 Dependent template arguments 14.6.3 Non-dependent names 14.6.4 Dependent name resolution 14.6.4.1 Point of instantiation 14.6.4.2 Candidate functions 14.6.5 Friend names declared within a class template
その前に
name lookup / name resolution
訳は名前解決あるいは名前検索や名前参照でしょうか
コンパイラが名前を探す仕組みのことを name lookup や name resolution といいます
蛇足:
ここで言っているのは「コンパイラの内部の動作」であるということに気を付けてください
コンパイルされたアプリケーションに、関数呼び出しのたびに呼び出すべき関数を探すような条件分岐のコードが生成されるわけではありません
Qualified name / Unqualified name
訳は修飾名と非修飾名でしょうか
std::swap の swap は std:: で修飾されている Qualified name ですが、std はその前に何の修飾もないので Unqualified name です
::std::swap の std はグローバル名前空間を指す :: で修飾されているので Qualified name です
この修飾というのは何を意味しているのかというと、これはコンパイラが名前を探す範囲を指定しているものです
Qualified name を探す仕組みを Qualified name lookup といい、Unqualified name を探す仕組みを Unqualified name lookup といいます
Qualified name lookup / Unqualified name lookup
訳は修飾名名前解決、非修飾名名前解決でしょうか
Qualified name lookup は名前を探す範囲が明示されているので、コンパイラは決まった候補の中から必要なものを探すだけです
コンパイラが std::swap から swap を探すとき、std:: で指定されている範囲を先に解決した上で、その範囲の中から swap という名前を探します
Unqualified name lookup は、名前を探す範囲が明示されていないので、コンパイラは標準で決められた候補を考慮して名前を探します
このとき探す名前が関数の名前である場合には Argument-dependent name lookup という仕組みも考慮します
Argument-dependent name lookup
ADL とも呼ばれる Argument-dependent name lookup は、関数呼び出しの実引数の属する namespace も名前検索に追加するという決まりです
というとなんだか難しそうですが、簡単です
これは主に std 名前空間にある stream クラスの operator<< や string クラスの operator+ などのためにあるような機能です
次の例を見てみましょう
namespace aaa { class cfoo { int m_value = 0; public: int get_value() const { return m_value; } cfoo& set_value( int value_new ){ m_value = value_new; return *this; } cfoo& add_value( int d ){ m_value += d; return *this; } cfoo& operator+=( int rhs ){ return add_value( rhs ); } cfoo():m_value(){} }; int operator+( const cfoo& lhs, int rhs ){ return lhs.get_value() + rhs; } } int main(){ aaa::cfoo foo; foo.set_value( 2 ); debug_printf( "%d\n", foo + 3 ); return 0; }
この例では foo + 3 とやっているのですが、なぜか何のエラーにもなりません
おかしいですね
呼び出し元の名前空間はグローバル名前空間ですが、グローバル名前空間には aaa 名前空間にある cfoo を引数に取るような operator+ はありません
プログラマの意図として aaa 名前空間にある operator+ が呼び出されてほしいのだろうというのはわかります
しかし、呼び出し元はグローバル名前空間なのですから、やはり本来であれば using aaa::operator+; というような指定が必要なはずです
なぜ指定してもいない aaa 名前空間にある関数が勝手に使われてしまっているのでしょうか
それはそこに Argument-dependent name lookup の仕組みが働いているからなのです
Argument-dependent name lookup は関数呼び出しに指定されている実引数を見て、それがどこかの名前空間に属しているものなら、呼び出すべき関数を探す候補にその名前空間も含めるという仕組みです
そうすることによってプログラマの意図を汲んだコード生成ができるようになっています
aaa 名前空間にある cfoo クラスのインスタンスが操作対象なのだから、呼び出そうとしている operator+ も同じ aaa 名前空間に置いてあるものが使われることをプログラマは期待しているだろう。と考えるのは自然なことでしょう
コンパイラはコードの記述からそのプログラマの期待を想定できるのだから、自動的にその期待に応えたコード生成をすればいいじゃないか、ということのようです
もし Argument-dependent name lookup がなかったらどうなるでしょうか
Argument-dependent name lookup がなかったとすると、どこかの名前空間にあるユーザー定義演算子を使いたい場合は using aaa::operator+; というような指定が必要になります
そうすると何らかのライブラリを使う際は、そのライブラリにある使いたいユーザー定義演算子を個別に using する必要が出てきてしまいます
面倒ですね
それをユーザーがいちいちやるのは大変だとなると、ライブラリを提供する側は必要な using を列挙したヘッダも一緒に提供するようになるかもしれません
これも面倒ですね
そんな面倒がないように Argument-dependent name lookup という仕組みが導入されたのでした
お蔭で std 名前空間にある stream 関連の operator<< や string 関連の operator+ などをいちいち using することなく使えるようになっているのです
と、これだけ聞くと Argument-dependent name lookup という仕組みはとても素晴らしいもののようですね
ただ、これが実はときどき難解な副作用を生む羽目になっていて困ることもあるのです
問題はこの Argument-dependent name lookup が、ユーザー定義演算子固有の特殊機能ではなく、一般の名前解決の仕組みに組み入れられてしまっていることです
次の例を見てみましょう
namespace version1 { struct foo_t { int old_data; }; void aaa( const foo_t& foo ){ debug_printf( "version1::aaa: old_data = %d\n", foo.old_data ); } } namespace version2 { struct foo_t { int new_data1; int new_data2; }; void aaa( const foo_t& foo ){ debug_printf( "version2::aaa: new_data1 = %d, new_data2 = %d\n", foo.new_data1, foo.new_data2 ); } } using namespace version2; int main(){ version1::foo_t foo = { 1 }; aaa( foo ); return 0; }
名前空間 version1 と version2 がありますが、通常使う名前空間として version2 が使われることを期待して using namespace version2; としています
version2::aaa は新しい foo_t が渡されることを期待した実装となっており、古い foo_t が渡されることは想定していません
新しい関数に古い型の変数が渡されたとしてもコンパイル時にコンパイルエラーになって検出される。というのがプログラマの意図でしょうか
しかし、実はこのコードはコンパイルエラーにはなりません
aaa に渡されているのは名前空間 version1 にある foo_t という型のインスタンスですから、ここで Argument-dependent name lookup の仕組みが働いて呼び出すべき関数の候補として version1::aaa も追加されます
version2::aaa に version1::foo_t を渡すことはできませんが、version1::aaa には version1::foo_t を渡すことができるので、コンパイラは version1::aaa を呼び出すようなコードを生成します
名前空間 version2 を使ってほしくて using namespace version2; していたのに無視されてしまいました
これが Argument-dependent name lookup の副作用です
異なる名前空間に同じ名前の関数がある場合、Argument-dependent name lookup の仕組みが働いて意図しない結果になる場合があります
これにテンプレートが絡むと全く意図していなかった名前空間にある同名の関数に食べられていたというようなことも起き得ます
問題が起きてもコンパイル時には検出できないので、実行時に意図しない挙動が起きてはじめて気づくということにもなり、なかなかやっかいです
Argument-dependent name lookup という仕組みは Unqualified name lookup のときに働く仕組みなので、Qualified name lookup を使うようにすれば回避できますが、常に全部修飾を明示してコードを書くというようなことはできませんね
何かおかしなことが起きたときは、Argument-dependent name lookup という仕組みのことも思い出してみると問題の特定に繋がるかもしれません
常に意識している必要はありませんが、頭のどこかには入れておきましょう
Two phase name lookup
コンパイラがテンプレート定義を解析する時、まずは単純にその記述自体を解析する段階があります
この段階ではテンプレート引数に関連するものは未知のものとして扱い、それ以外の要素についてだけ解析を進めます
その後、そのテンプレートが実際に使われている箇所、これをテンプレートのインスタンス化といいますが、テンプレートがインスタンス化され、テンプレート引数が確定した段階での解析があります
これはヘッダにあるテンプレートクラスのソースを解析する段階と、それを使っているソースを解析する段階の二つがあると考えると分かり易いでしょうか
ずっと昔からあるテンプレートクラスを使ったのになぜか初めてのコンパイルエラーが出たということを経験した方もいるのではないでしょうか
これはテンプレートの解析が二段階になっているためです
またテンプレートクラスの基底クラスがテンプレート引数に関係したものだと、コンパイラが基底クラスのメンバ関数を認識してくれないというのもこのためです
テンプレート引数に関連したものを使いたい場合は、予め using しておくことでそれが dependent name であると明示するか
template<typename T> struct foo_t { typedef T value_type; value_type value; }; template<typename T> struct cfoo : public foo_t<T> { using typename foo_t<T>::value_type; using foo_t<T>::value; value_type get_value() const { return value; } void set_value( value_type value_new ){ value = value_new; } }; int main(){ cfoo<int> foo; foo.set_value( 1 ); debug_printf( "%d\n", foo.get_value()); return 0; }
あるいは明示的に基底クラスのものであることを指定するか
template<typename T> struct foo_t { typedef T value_type; value_type value; }; template<typename T> struct cfoo : public foo_t<T> { using typename foo_t<T>::value_type; value_type get_value() const { return foo_t<T>::value; } void set_value( value_type value_new ){ foo_t<T>::value = value_new; } }; int main(){ cfoo<int> foo; foo.set_value( 1 ); debug_printf( "%d\n", foo.get_value()); return 0; }
あるいは this-> を指定することで dependent name であることを指定するか
template<typename T> struct foo_t { typedef T value_type; value_type value; }; template<typename T> struct cfoo : public foo_t<T> { using typename foo_t<T>::value_type; value_type get_value() const { return this->value; } void set_value( value_type value_new ){ this->value = value_new; } }; int main(){ cfoo<int> foo; foo.set_value( 1 ); debug_printf( "%d\n", foo.get_value()); return 0; }
など、何らかの方法で評価をテンプレートのインスタンス化まで遅らせる必要があります
このテンプレートでの名前解決が二段階になっている仕組みを Two phase name lookup というそうです
用語の出典はどこかなあ