1. ホーム
  2. c++

[解決済み】なぜthisポインタを介してテンプレートベースクラスのメンバにアクセスしなければならないのでしょうか?

2022-04-17 15:51:37

質問

もし、以下のクラスがテンプレートでなければ、単純に以下のようにすることができます。 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;
}

解決方法は?

簡単に言うと x は従属名であり、テンプレート・パラメータが判明するまで検索は延期されます。

長い答え:コンパイラはテンプレートを見たとき、テンプレート・パラメータを見ずに特定のチェックを即座に実行することになっています。その他のチェックは、パラメータがわかるまで延期されます。これは二相コンパイルと呼ばれ、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 がオブジェクトの場合、それは乗算です(そして、何らかの演算子のオーバーロードを除けば、rvalue に代入することは違法です)。もしそれが間違っていれば、このエラーは診断されなければなりません。 フェーズ1 標準ではエラーと定義されています。 テンプレート内の 特定のインスタンス化ではなく、そのインスタンス化です。たとえテンプレートが一度もインスタンス化されなかったとしても,A が int の場合、上記のコードは不正な形式であり、診断されなければなりません。 foo はテンプレートではなく、単なる関数です。

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

T::A はTに依存する名前になります。それが型であるかどうかは、フェーズ1ではわからないでしょう。最終的に使われることになる型は T また、仮に定義されていたとしても、どの型がテンプレート・パラメータとして使われるかはわかりません。しかし、貴重なフェーズ1で不正なテンプレートをチェックするために、文法を解決しなければなりません。そこで、標準規格には従属名に関するルールがあります。 typename と指定することで の型であるか、あるいは特定の曖昧でない文脈で使用されます。例えば template <typename T> struct Foo : T::A {}; , T::A は基底クラスとして使われるため、一義的に型である。もし Foo を持つ何らかの型のインスタンス化です。 A ネストされたタイプAではなく、インスタンス化を行うコード(フェーズ2)のエラーであり、テンプレート(フェーズ1)のエラーではありません。

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

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

Aは従属名なのか、そうでないのか?ベースクラスと 任意の という名前がベースクラスに出現する可能性があります。そこで、Aを従属名とし、非型として扱うことができる。この場合、望ましくない効果として すべての名前 は従属であり、従って 各タイプ Foo で使用される型(組み込み型を除く)は、修飾する必要があります。Fooの内部では、こう書かなければならないだろう。

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

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

好みのコードを許可することの第二の問題点 ( return x; ) は、たとえ Bar の前に定義されている Foo であり、かつ x がその定義のメンバでない場合、誰かが後で Bar ある型に対して Baz というような Bar<Baz> はデータメンバ x をインスタンス化し、さらに Foo<Baz> . したがって、そのインスタンス化において、テンプレートは、グローバルな x . また逆に、ベースとなるテンプレートの定義が Bar がありました。 x を定義することができ、テンプレートはグローバルな x で返すようにします。 Foo<Baz> . これは、あなたの抱えている問題と同じように驚き、悩むと判断したのだと思いますが、それは 静かに 驚くようなエラーを投げるのとは対照的です。

このような問題を避けるために、この規格では、クラス・テンプレートの従属基底クラスは、明示的に要求されない限り、検索対象として考慮されないと言うことにしています。これによって、依存する基底クラスで見つかるかもしれないという理由だけで、すべてが依存することを止めます。また、あなたが見ているような好ましくない効果もあります。つまり、ベースクラスから何かを認定しなければ、それが見つからないということです。を作るには、3つの一般的な方法があります。 A に依存します。

  • using Bar<T>::A; をクラスで使用します。 A にあるものを参照しています。 Bar<T> 従って、依存する。
  • Bar<T>::A *x = 0; at point of use - もう一度。 A は間違いなく Bar<T> . これは乗算です。 typename が使われていないので、もしかしたら悪い例かもしれませんが、インスタンス化するまでは operator*(Bar<T>::A, x) は rvalue を返します。もしかしたら、そうかもしれません。
  • this->A; at point of use - (使用時 A はメンバーなので、もしそれが Foo 標準では、これは依存関係にあるとされています。

二相コンパイルは厄介で難しく、コードに余分な文言が必要になることもあります。しかし、むしろ民主主義のように、他のすべての方法を除けば、おそらく最悪の方法なのです。

あなたの例では、合理的に主張することができます。 return x; は意味をなさない。 x は基底クラスのネストした型なので、言語は (a) 従属名であると言い、 (2) 非型として扱うべきであり、あなたのコードは this-> . しかし、ベースクラスがグローバルのシャドウとなる名前を導入する可能性がある、あるいは、持っていると思っていた名前を持たず、代わりにグローバルが見つかるという問題が残っているのです。

また、依存する名前については、デフォルトはその逆であるべきだと主張することもできます(何らかの方法でオブジェクトであると指定されない限り、型を仮定する)、またはデフォルトはより文脈に敏感であるべきだと主張することもできます(例えば std::string s = ""; , std::string は、文法的に意味をなさないので、型として読むことができます。 std::string *s = 0; は曖昧である)。繰り返しになりますが、このルールがどのように合意されたのか、私はよく知りません。私の推測では、どの文脈が型をとり、どの文脈が型でないかという具体的なルールをたくさん作ると、テキストのページ数が増えてしまうので、それを軽減するためではないかと思います。