逆にどうしてコンパイルが通っていたのか?
私は本稿を書くにあたって、冒頭の「識別子がみつかりませんでした」を再現するためにソースコードを書いて、Visual Studio 2026 で動作確認をしました。
「識別子がみつかりませんでした」はすぐに再現できるのですが、コンパイルが通っていた再現はなかなかできませんでした。
C++を 20から 14 や 11 までに戻すのですが、やっぱり「識別子がみつかりませんでした」が出ます。
それでもいろいろ調べてようやく、C++ のバージョンの問題ではなくて、MSVC の規格に準拠していない動作が原因だったということがわかりました。
上で「MSVC では X::type と書かれていたときの解釈をインスタンス生成時にまで後回しにしていたと」と書きましたが、これのことです。typename が必要になったのは Visual Studio 2017 でした。
しかし、VS2017 でもまだ不十分だったのです。それがこの問題の member という依存名のコンパイルを通していたという動作です。
このときの規格に準拠しない動作は、MSVC の
/permissive
準拠モード → いいえ
というオプションで再現できます。
これでコンパイルすると member の識別子はみつかります。
逆に memberという識別子を見つけない、C++の規格に準拠した動作は以下の設定にします。
/permissive-
準拠モード → はい
GUI では
プロジェクトのプロパティ → 構成プロパティ → C/C++ → 言語 → 準拠モード
で確認できます。
つまり、C++17 から C++20 に切り替えたときにおそらく自動的に準拠モードも「準拠しない」から「準拠する」に変わったことで二段階名前探索が徹底され、それによって「識別子がみつかりませんでした」になっていたというわけです。
これで冒頭の問題は解決しました。しかし、
「もうちっとだけ続くんじゃ」
ADL(Argument-Dependent Lookup)
二段階名前探索に関連して、ついでに ADL もご紹介しましょう。
古くからあるけれど案外と知られていない便利な C++ の機能です。
ADL、日本語にすると「引数依存名前探索」となります。
以下のソースコードを御覧ください。
#include <iostream>
namespace MATSUYA_GROUP {
class NOYA {
public:
const char* ushimeshi ="NOYAとは松のやのこと";
};
void put( const NOYA& noya ) {
std::cout << noya.ushimeshi << std::endl;
}
}
int main()
{
MATSUYA_GROUP::NOYA noya;
MATSUYA_GROUP::put( noya );
}
このコードはコンパイルは通るし、実行もできます。
MATSUYA_GROUP という名前空間の中にある NOYA クラスを引数にして、
MATSUYA_GROUP::put というフリー関数を呼んでいます。
この
MATSUYA_GROUP::put( noya );
のスコープ解決演算子を外すとどうなるでしょうか?
int main()
{
MATSUYA_GROUP::NOYA noya;
put( noya );
}
実はこれでも大丈夫です。 put という関数は MATSUYA_GROUP という名前空間にしかありませんが、この関数の引数型である MATSUYA_GROUP::NOYA の属する名前空間から探し出して解決しているのです。
これを「引数依存名前探索」、ADL(Argument-Dependent Lookup)
といいます。
引数の型と同じ名前空間からも探索するというルールです。
アンドリュー・ケーニッヒ氏によって提案されたことから「Koenig Lookup(ケーニッヒ探索)」ともいいます。
関数の場合だったら、MATSUYA_GROUP::をつけるくらい造作でもないので、ADLの必要性はあまり感じません。しかし、非メンバ演算子の場合は
どうでしょうか?
#include <iostream>
namespace MATSUYA_GROUP {
class NOYA {
public:
const char* ushimeshi ="NOYAとは松のやのこと";
};
std::ostream& operator<<( std::ostream& os, const NOYA& obj )
{
return os << obj.ushimeshi;
}
}
int main()
{
MATSUYA_GROUP::NOYA noya;
std::cout << noya << std::endl;
}
このように、
std::cout << noya << std::endl;
と書けると便利ですよね。実際、ADL によりこう書けるのです。
ADL がないと、
MATSUYA_GROUP::operator<<( std::cout, noya );
または
using namespace MATSUYA_GROUP;
std::cout << noya << std::endl;
または
using namespace MATSUYA_GROUP::operator<<;
std::cout << noya << std::endl;
と書かなければなりません。
ええ、書いてましたとも。ADL を知る前の私はこのように書いてました。
二段階名前探索と ADL
さて、二段階名前探索とは
- コンパイラがテンプレートの定義をみつけたときの第一フェーズ
- テンプレートのインスタンス化を行うときの第二フェーズ
の2つのフェーズで識別子の解決を行うことです。
この第二フェーズでのテンプレートパラメータ X が決まったときの関数呼び出し(関数および、関数テンプレート)と非メンバ演算子の探索がテンプレートパラメータ X が定義された名前空間も対象にするということです。
つまり、テンプレート内で foo(x) と書いたとき、foo がテンプレート引数に依存する名前であれば、その定義がインスタンス化されるまで保留され、最終的に実引数の型に基づいたADLによって解決されるのです。
ちなみに関数のように見える
- 関数ポインタ
- 関数オブジェクト
- std::function
- ラムダ式
などは、関数外に定義宣言されているものでも ADL の対象ではありません。これらは関数ではなく、変数として扱われるためです。
以上で、本稿で私が説明したかったことは書き尽くしました。
最後までお読みいただき、ありがとうございました。
(終了)