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

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

ただいまの
回答率

89.99%

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

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 5
  • VIEW 3,092

tiitoi

score 12939

環境

  • 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」という仕様によるものであるということをご教示いただきました。

#include <iostream>

// 親クラス
template <class Child, class SomeType>
struct Base
{
    void interface()
    {
        static_cast<Child *>(this)->implementation();
    }

    void set_value(SomeType value)
    {
        member_ = value;
    }

  public:
    SomeType member_;
};

// 子クラス
template <class SomeType>
struct Derived : Base<Derived<SomeType>, SomeType>
{
    void implementation()
    {
        std::cout << member_ << std::endl;
    }
};

int main(int argc, char *argv[])
{
    Derived<int> obj;
    obj.set_value(1);
    obj.interface();
}

なぜ 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」が必要になった理由

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

例:

#include <cstdio>

void func(void*) { std::puts("The call resolves to void*") ;}

template<typename T> void g(T x)
{
    func(0);
}

void func(int) { std::puts("The call resolves to int"); }

int main()
{
    g(3.14);
}
  • 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: テンプレートが定義された段階では曖昧さがあっても、実体化された段階で曖昧さがないケース

#include <cstdio>
#include <vector>

template<class C>
void f(const C &container) {
  C::const_iterator *x;
}

int main()
{
    std::vector<int> v;
    f(v);
}

テンプレートが実体化されていない段階では、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 と指定する必要がある。
#include <cstdio>
#include <vector>

template<class C>
void f(const C &container) {
  C::const_iterator *x;
}

int main()
{
    std::vector<int> v;
    f(v);
}

gcc:

source_file.cpp: In functionvoid 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 は型と判明しているのでエラーにならない。 

参考文献

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • Chironian

    2019/03/14 22:06

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

    キャンセル

  • tiitoi

    2019/03/15 13:21

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

    キャンセル

回答 1

checkベストアンサー

+6

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

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

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

#include <cstdio>

void func(void*) { std::puts("The call resolves to void*") ;}

template<typename T> void g(T x)
{
func(0);
}

void func(int) { std::puts("The call resolves to int"); }

int main()
{
g(3.14);
}

出力:

The call resolves to void*

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/03/14 23:24

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

    キャンセル

  • 2019/03/15 13:17 編集

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

    キャンセル

  • 2019/03/15 15:50 編集

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

    キャンセル

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

  • ただいまの回答率 89.99%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる