テンプレート基本クラスのメンバーに this ポインターを介してアクセスする必要があるのはなぜですか? 質問する

テンプレート基本クラスのメンバーに this ポインターを介してアクセスする必要があるのはなぜですか? 質問する

x以下のクラスがテンプレートでない場合は、クラスに を含めるだけで済みますderived。ただし、以下のコードではを使用する必要がありますthis->x。なぜでしょうか?

template <typename T>
class base {

protected:
    int x;
};

template <typename T>
class derived : public base<T> {

public:
    int f() { return this->x; }
};

int main() {
    derived<int> d;
    d.f();
    return 0;
}

ベストアンサー1

短い答え: 依存名を作成してx、テンプレート パラメータがわかるまで検索を延期するためです。

長い答え: コンパイラがテンプレートを見つけると、テンプレート パラメータを確認せずに、特定のチェックをすぐに実行することになっています。その他のチェックは、パラメータが判明するまで延期されます。これは 2 フェーズ コンパイルと呼ばれ、MSVC では実行されませんが、標準では必須であり、他の主要なコンパイラでは実装されています。必要に応じて、コンパイラはテンプレートを見つけるとすぐに (何らかの内部解析ツリー表現に) コンパイルし、インスタンス化のコンパイルを後回しにする必要があります。

テンプレートの特定のインスタンスではなく、テンプレート自体に対して実行されるチェックでは、コンパイラがテンプレート内のコードの文法を解決できることが求められます。

C++ (および C) では、コードの文法を解決するために、何かが型であるかどうかを知る必要がある場合があります。例:

#if WANT_POINTER
    typedef int A;
#else
    int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }

A が型の場合、それはポインターを宣言します (グローバル をシャドウする以外の効果はありませんx)。 A がオブジェクトの場合、それは乗算です (一部の演算子のオーバーロードを除いて、右辺値への割り当ては違法です)。これが間違っている場合、このエラーはフェーズ 1 で診断されなければなりません。これは標準によってテンプレートのエラーとして定義されており、特定のインスタンス化のエラーではありません。テンプレートがインスタンス化されない場合でも、 A が の場合、上記のコードは不正な形式であり、がテンプレートではなく単純な関数であるint場合と同じように診断されなければなりません。foo

さて、標準では、テンプレート パラメータに依存しない名前はフェーズ 1 で解決可能でなければならないとされています。Aここでは依存名ではなく、型に関係なく同じものを参照しますT。したがって、フェーズ 1 で検出およびチェックされるようにするには、テンプレートが定義される前に定義する必要があります。

T::Aは、T に依存する名前になります。フェーズ 1 で、それが型であるかどうかを知ることは不可能です。Tインスタンス化で最終的に として使用される型は、おそらくまだ定義されておらず、たとえ定義されていたとしても、どの型がテンプレート パラメーターとして使用されるかはわかりません。しかし、フェーズ 1 で不正な形式のテンプレートをチェックするために、文法を解決する必要があります。したがって、標準には依存名に関するルールがあります。つまり、 で修飾されて型であると指定されているか、特定の明確なコンテキストで使用されている場合を除き、コンパイラーは、それらが非型typenameである想定する必要があります。たとえば、 ではtemplate <typename T> struct Foo : T::A {};T::Aは基本クラスとして使用されているため、明確に型です。 が、ネストされた型 A ではなくFoo、データ メンバーを持つ型でインスタンス化されるA場合、それはインスタンス化 (フェーズ 2) を実行するコードのエラーであり、テンプレートのエラー (フェーズ 1) ではありません。

しかし、依存する基本クラスを持つクラス テンプレートの場合はどうでしょうか?

template <typename T>
struct Foo : Bar<T> {
    Foo() { A *x = 0; }
};

A は従属名ですか? 基底クラスでは、任意の名前が基底クラスに出現する可能性があります。したがって、A は従属名であるとし、非型として扱うことができます。これは、 Foo 内のすべての名前が従属的であるという望ましくない効果をもたらし、したがってFoo で使用されるすべての型(組み込み型を除く) を修飾する必要があります。Foo の内部では、次のように記述する必要があります。

typename std::string s = "hello, world";

従属名であるためstd::string、特に指定しない限り非型とみなされます。痛い!

優先コード ( return x;) を許可することによる 2 つ目の問題は、Barが より前に定義されていてFoo、 がxその定義のメンバーでない場合でも、誰かが後からBar何らかの型 に対しての特殊化を定義してBaz、 がBar<Baz>データ メンバー を持つようにしx、 をインスタンス化できることFoo<Baz>です。そのため、そのインスタンス化では、テンプレートはグローバル を返す代わりに、データ メンバーを返しますx。または逆に、 の基本テンプレート定義に がBarあった場合、 なしで特殊化を定義することができ、テンプレートはで返すxグローバルを探します。これは、あなたが抱えている問題と同じくらい驚きで困惑するものと判断されたと思いますが、驚くべきエラーをスローするのではなく、静かに驚くことです。xFoo<Baz>

これらの問題を回避するために、標準では、クラス テンプレートの依存基本クラスは、明示的に要求されない限り、検索対象として考慮されないと規定されています。これにより、依存基本クラスで見つかる可能性があるという理由だけで、すべてが依存する状態になることがなくなります。また、ベース クラスからのものに対して修飾を行う必要があるという望ましくない効果も発生します。A依存させる一般的な方法は 3 つあります。

  • using Bar<T>::A;クラス内の - はA現在 内の何かを参照しておりBar<T>、したがって依存しています。
  • Bar<T>::A *x = 0;使用時点で - 繰り返しますが、Aは間違いなく に含まれますBar<T>。 はtypename使用されなかったので、これは乗算であり、おそらく悪い例ですが、 が右辺値を返すかどうかを調べるにはインスタンス化まで待たなければなりませんoperator*(Bar<T>::A, x)。 返されるかもしれません...
  • this->A;使用時点では - はAメンバーであるため、 にない場合はFoo基本クラスに存在する必要があります。この場合も、標準ではこれが依存することになります。

2 段階コンパイルは面倒で難しく、コードに余分な言葉遣いが必要になるという驚くべき要件が生じます。しかし、民主主義と同様、他のすべての方法と比べると、おそらく最悪のやり方です。

あなたの例では、が基本クラスでネストされた型であるreturn x;場合、 は意味をなさないのでx、言語は (a) 依存名であると述べ、(2) それを非型として扱うべきであり、あなたのコードは がなくても動作するだろうと合理的に主張できますthis->。ある程度、あなたはあなたのケースには当てはまらない問題の解決策による付随的な被害の被害者ですが、それでも、あなたの基本クラスがグローバルをシャドウする名前をあなたの下に導入する可能性がある、または、あなたが持っていると思っていた名前がなく、代わりにグローバルが見つかるという問題が残っています。

また、依存する名前についてはデフォルトをその逆 (何らかの方法でオブジェクトとして指定されない限り、 型と想定) にするべき、またはデフォルトをもっとコンテキスト依存にするべき ( では、 はstd::string s = "";あいまいstd::stringであっても、他に文法的に意味をなさないものがあるため、 を型として読み取ることができるstd::string *s = 0;) と主張することもできます。 繰り返しますが、ルールがどのように合意されたのかはよくわかりません。 私の推測では、必要なテキストのページ数を考えると、どのコンテキストが型を取り、どのコンテキストが非型を取るかについての多くの特定のルールを作成することは軽減されるでしょう。

おすすめ記事