質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
GCC

GCCはGNU Compiler Collectionの略です。LinuxのC言語コンパイラのデファクトスタンダードであり、数多くの他言語やプラットフォームサポートもします。

Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

2回答

2514閲覧

C++ - Two phase name lookup という言語仕様はなぜ必要なのでしょうか

tiitoi

総合スコア21956

GCC

GCCはGNU Compiler Collectionの略です。LinuxのC言語コンパイラのデファクトスタンダードであり、数多くの他言語やプラットフォームサポートもします。

Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

8グッド

7クリップ

投稿2019/03/14 10:14

編集2019/03/15 06:48

環境

  • C++11
  • Visual Studio 2017 15.8.1
  • gcc version 5.4.0

概要

「two-phase name lookup」に関する自分の理解は以下の通りです。

  • 先にテンプレート引数に依存しないものを名前解決する。
  • 次にテンプレート引数に依存するものを名前解決する。

しかし、なぜ2段階で名前解決する必要があるのでしょうか。
「two-phase name lookup」をしないで、全部解析してから名前解決を行うのではなにか問題があるのでしょうか。

質問の背景

以下のコードが Visual C++ ではコンパイルが通り、gcc では通らない原因について「C++ - gcc でコンパイルすると、親クラスのメンバー変数を子クラスから参照できない問題について」で質問させていただきました。
そうしたところ、「two-phase name lookup」という仕様によるものであるということをご教示いただきました。

cpp

1#include <iostream> 2 3// 親クラス 4template <class Child, class SomeType> 5struct Base 6{ 7 void interface() 8 { 9 static_cast<Child *>(this)->implementation(); 10 } 11 12 void set_value(SomeType value) 13 { 14 member_ = value; 15 } 16 17 public: 18 SomeType member_; 19}; 20 21// 子クラス 22template <class SomeType> 23struct Derived : Base<Derived<SomeType>, SomeType> 24{ 25 void implementation() 26 { 27 std::cout << member_ << std::endl; 28 } 29}; 30 31int main(int argc, char *argv[]) 32{ 33 Derived<int> obj; 34 obj.set_value(1); 35 obj.interface(); 36}

なぜ Visual C++ ではビルドが通ったのか調べたところ、Visual C++ 15.3 以前では「two-phase name lookup」が実装されておらず、15.3 以降でもデフォルトでは無効になっており、/permissive- コンパイラオプションを有効にすると、機能するとのことでした。

C++ Team Blog | Two-phase name lookup support comes to MSVC

なので、試しに /permissive- オプションを有効にしてビルドしたところ、gcc と同様、名前解決に失敗することが確認できました。

エラー: C3861: 'member_': 識別子が見つかりませんでした

ここで思ったのは、VC++ でデフォルトでは「two-phase name lookup」をしないで、名前解決できているのだから、なぜこの仕様が必要なのかというのが質問に至った背景です。

すみませんが、よろしくおねがいします。

回答いただいた内容に関する理解

alphya さんにご回答いただいた内容を自分の勉強用に整理しました。
もし誤った解釈があれば、ご指摘いただければ幸いです。


  • テンプレートの元々の意図は、型が異なり内容が同じクラスや関数を複数作る必要がないようにするため。(例: sqrt, sqrtf, sqrtl)
  • 昔はテンプレート引数に非依存の識別子を先に名前解決することは、許可及び推奨されていたが、必須ではなかった。
  • そのため、識別子の名前解決はテンプレートの定義の解析時には行わないで、テンプレートを実体化するまで名前解決を遅らせることができた。

「two-phase name lookup」が必要になった理由

テンプレートの実体化時に名前解決を行う方法だと、テンプレートが前後の文脈に依存する状況がおき、バグを引き起こしやすくなる。

例:

cpp

1#include <cstdio> 2 3void func(void*) { std::puts("The call resolves to void*") ;} 4 5template<typename T> void g(T x) 6{ 7 func(0); 8} 9 10void func(int) { std::puts("The call resolves to int"); } 11 12int main() 13{ 14 g(3.14); 15}
  • two-phase name lookup を行う場合

テンプレートが定義された段階で template<typename T> void g(T x) の中身の解析が行われる。
この時点で func(0) にマッチするものは、void func(void*) だけなので、void func(void*) に名前解決される。

  • two-phase name lookup を行わない場合

g(3.14) でテンプレートが実体化された時点で template<typename T> void g(T x) の中身の解析が行われる。
この時点で func(0) にマッチするものは、void func(int)void func(void*) の2つがあるので、void func(int) に名前解決される。
もし、void func(int) がなかったならば、void func(void*) に名前解決される。
このように、テンプレートの後に void func(int) があるかどうかでテンプレートの意味が変わってしまう。 (前後の文脈に依存してしまう)

MSVC のバージョン 15.3 以前の実装

  • テンプレートは宣言だけチェックして、定義の部分は記録だけしておき、実体化されるまで中身のチェックはしない。
  • テンプレートの実体化が必要になった段階で、記録してあるリストからテンプレートの実装を探してきて、中身をチェックする。

例1: テンプレートの中身は、実体化されるまでチェックされない。

#include <cstdio> template <typename T> void hoge(T t) { 1 // ; がない } int main() { }

gcc: source_file.cpp:5:6: error: expected ';' after expression
vc++: コンパイルが通る。

例2: テンプレートが定義された段階では曖昧さがあっても、実体化された段階で曖昧さがないケース

cpp

1#include <cstdio> 2#include <vector> 3 4template<class C> 5void f(const C &container) { 6 C::const_iterator *x; 7} 8 9int main() 10{ 11 std::vector<int> v; 12 f(v); 13}

テンプレートが実体化されていない段階では、C::const_iterator は型なのかクラス C で定義された static 定数なのかわからない。

  • C::const_iterator が型と解釈した場合: C::const_iterator *x; は型 C::const_iterator のポインタ *x
  • C::const_iterator が定数と解釈した場合: C::const_iterator *x; は定数 C::const_iterator と変数 x の乗算

C::const_iterator を型と認識させるには typename C::const_iterator と指定する必要がある。

cpp

1#include <cstdio> 2#include <vector> 3 4template<class C> 5void f(const C &container) { 6 C::const_iterator *x; 7} 8 9int main() 10{ 11 std::vector<int> v; 12 f(v); 13}

gcc:

source_file.cpp: In function ‘void f(const C&)’: source_file.cpp:6:22: error: ‘x’ was not declared in this scope C::const_iterator *x; ^ source_file.cpp: In instantiation of ‘void f(const C&) [with C = std::vector<int>]’: source_file.cpp:12:8: required from here source_file.cpp:6:21: error: dependent-name ‘C:: const_iterator’ is parsed as a non-type, but instantiation yields a type C::const_iterator *x; ^ source_file.cpp:6:21: note: say ‘typename C:: const_iterator’ if a type is meant

vc++: コンパイルが通る。

two-phase name lookup を行う gcc は実体化前に解析するのでエラーとなるが、MSVC は実体化された段階で解析するので、この時点では C::const_iterator は型と判明しているのでエラーにならない。

参考文献

k-kuma.esd, Chironian, takumiabe, tsuyoshi_cho, yohhoy, SAM-tak, KN2018, alphya👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Chironian

2019/03/14 13:06

全部2フェーズ目に名前解決すればよいのにと思うので、私も知りたいです。歴史的経緯で修正したくてもできないのでは?と想像をたくましくしてますが、本当のところを知りたいです。
tiitoi

2019/03/15 04:21

コメントありがとうございます。 ふとした疑問ではあったのですが、C++の構文解析手法等も関わる根が深い問題だったのかもしれないですね。
guest

回答2

0

ベストアンサー

Two phase name lookup for C++ templates - Why? - Stack Overflow

上記に同様の質問がありました。そこによると、

  • テンプレート内のシンボルを2つのカテゴリ(従属シンボルと非従属シンボル)に分類し、テンプレートの定義時に非依存シンボルを解決して、いくつかのローカル実装のシンボルに誤ってバインドされるリスクを減らす
  • 依存シンボルに適切な場合は typenametemplate を指定するという要件と組み合わせることで、テンプレートがインスタンス化されたときだけではなく、テンプレートの定義時に解析とエラーチェックも可能になる(google翻訳)

とのことです。

最初の項目は、C++ Team Blog | Two-phase name lookup support comes to MSVCのリンクにもある、N0906 A Proposed New Template Compilation Model にも書いてありました。

理論的根拠:テンプレート作成者が以下の型を参照している場合

テンプレート定義の文脈では完全に知られている
彼が適用しようとしている操作を知っていると仮定し、
テンプレートユーザのコンテキストからの貢献はどれも
偶然です。 (google 翻訳)

具体的な例も、C++ Team Blog | Two-phase name lookup support comes to MSVCにある通りです。次の場合、テンプレート定義の時点で、テンプレートを書いた人は既に知られているfunc(void*)を期待しているでしょう。しかし、Two-phase name lookup がない場合はfunc(int)に解決してしまいます。

cpp

1#include <cstdio> 2 3void func(void*) { std::puts("The call resolves to void*") ;} 4 5template<typename T> void g(T x) 6{ 7func(0); 8} 9 10void func(int) { std::puts("The call resolves to int"); } 11 12int main() 13{ 14g(3.14); 15}

出力:

The

1

投稿2019/03/14 13:59

編集2019/03/14 14:09
alphya

総合スコア124

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

alphya

2019/03/14 14:24

N0906 を読むと、ODR にも関係しそうな気がします。私も今初めて調べたので、他の方の解釈も気になります。
tiitoi

2019/03/15 04:27 編集

ご回答ありがとうございます。 記載いただいた内容を理解、確認しようと思います。(少々時間がかかるかもしれません)
tiitoi

2019/03/15 06:50 編集

内容についておおよその理解ができました。 意図しない名前解決が行われないようにするために導入されたルールなのですね。 ご回答ありがとうございました。
guest

0

”例1: テンプレートの中身は、実体化されるまでチェックされない" は、少なくとも本投稿時点の下記著名コンパイラのコンパイル結果と矛盾しました。実体化されずとも、テンプレート定義が検査されて構文エラーになりました。両方、c++23モードです。言語仕様が変わったかもしれません。
・VS2023のVC++( MSVC )
・clang 16

それを補う説明が、下記URL内で「Without Instantiation the template code itself is checked for syntax. Eg: Any syntax errors such as ; etc.」とあります。
https://stackoverflow.com/questions/7767626/two-phase-lookup-explanation-needed

あと、"「two-phase name lookup」が必要になった理由"は、「テンプレートコードの解析精度・堅牢性を上げるため」という事でした。

投稿2024/02/12 08:07

ButaDon

総合スコア4

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問