なぜこれはコンパイルするのですか?
class FooBase
{
protected:
void fooBase(void);
};
class Foo : public FooBase
{
public:
void foo(Foo& fooBar)
{
fooBar.fooBase();
}
};
しかし、これはしませんか?
class FooBase
{
protected:
void fooBase(void);
};
class Foo : public FooBase
{
public:
void foo(FooBase& fooBar)
{
fooBar.fooBase();
}
};
一方では、C++はそのクラスのすべてのインスタンスのプライベート/保護されたメンバーへのアクセスを許可しますが、他方では、サブクラスのすべてのインスタンスの基本クラスの保護されたメンバーへのアクセスを許可しません。これは私にはかなり矛盾しているように見えます。
VC++とideone.comでのコンパイルをテストしましたが、どちらも最初のコードスニペットをコンパイルしますが、2番目のコードスニペットはコンパイルしません。
foo
がFooBase
参照を受け取った場合、コンパイラは引数がFoo
の子孫であるかどうかを認識していないため、そうでないと想定する必要があります。 Foo
はother Foo
objectsの継承された保護メンバーにアクセスできますが、他のすべての兄弟クラスにはアクセスできません。
このコードを考えてみましょう:
class FooSibling: public FooBase { };
FooSibling sib;
Foo f;
f.foo(sib); // calls sib.fooBase()!?
Foo::foo
は、任意のFooBase
の子孫の保護されたメンバーを呼び出すことができます。その後、FooSibling
と直接関係のないFoo
の保護されたメソッドを呼び出すことができます。これは、保護されたアクセスが機能するはずの方法ではありません。
Foo
が、すべてのFooBase
オブジェクトの保護されたメンバーにアクセスする必要がある場合、Foo
の子孫であることがわかっているオブジェクトだけでなく、Foo
はFooBase
の友達:
class FooBase
{
protected:
void fooBase(void);
friend class Foo;
};
C++ FAQ はこの問題をうまくまとめています:
[あなた]は自分のポケットを選ぶことができますが、父親のポケットや兄弟のポケットを選ぶことはできません。
重要な点は、protected
は、-any他のオブジェクトのメンバーではなく、メンバーの独自のコピーへのアクセスを許可することです。 protected
はメンバーに派生型へのアクセスを許可することを一般化して述べるので、これはよくある誤解です(自分のベースのみに明示的に言及せずに...)
さて、これは理由によるものであり、他のオブジェクトが依存している不変式を壊す可能性があるため、通常は階層の別のブランチのメンバーにアクセスしないでください。いくつかの大きなデータメンバー(保護されている)で負荷の高い計算を実行する型と、さまざまな戦略に従って結果をキャッシュする2つの派生型について考えます。
class base {
protected:
LargeData data;
// ...
public:
virtual int result() const; // expensive calculation
virtual void modify(); // modifies data
};
class cache_on_read : base {
private:
mutable bool cached;
mutable int cache_value;
// ...
virtual int result() const {
if (cached) return cache_value;
cache_value = base::result();
cached = true;
}
virtual void modify() {
cached = false;
base::modify();
}
};
class cache_on_write : base {
int result_value;
virtual int result() const {
return result_value;
}
virtual void modify() {
base::modify();
result_value = base::result();
}
};
cache_on_read
タイプは、データへの変更をキャプチャし、結果を無効としてマークします。これにより、値の次のreadが再計算されます。オンデマンドでのみ計算を実行するため、書き込みの数が比較的多い場合、これは良いアプローチです(つまり、複数の変更によって再計算がトリガーされることはありません)。 cache_on_write
事前に結果を事前計算します。これは、書き込みの数が少なく、読み取りの確定的なコストが必要な場合に適しています(読み取りのレイテンシが低いと考えてください)。
ここで、元の問題に戻ります。どちらのキャッシュ戦略も、ベースよりも厳しい不変条件のセットを維持します。最初のケースでは、最後の読み取り後にcached
が変更されていない場合にのみ、true
はdata
になります。 2番目のケースでは、追加の不変式はresult_value
は、常に操作の値です。
3番目の派生型がbase
への参照を取得し、data
にアクセスして書き込みを行った場合(protected
で許可されている場合)、派生型の不変式で壊れます。 。
そうは言っても、言語の仕様はbroken(個人的な意見)であり、その特定の結果を達成するためのバックドアを残しています。特に、派生型のベースからメンバーのメンバーへのポインターを作成する場合、アクセスはderived
でチェックされますが、返されるポインターはbase
のメンバーへのポインターです。 anybase
オブジェクトに適用できます:
class base {
protected:
int x;
};
struct derived : base {
static void modify( base& b ) {
// b.x = 5; // error!
b.*(&derived::x) = 5; // allowed ?!?!?!
}
}
どちらの例でも、Foo
は保護されたメソッドfooBase
を継承します。ただし、最初の例では、同じクラスから指定された保護されたメソッドにアクセスしようとしますが(Foo::foo
はFoo::fooBase
を呼び出します)、2番目の例では、別のクラスから保護されたメソッドにアクセスしようとしますtはフレンドクラスとして宣言されています(Foo::foo
はFooBase::fooBase
を呼び出そうとしますが、失敗し、後者は保護されます)。
最初の例では、タイプFooのオブジェクトを渡します。これは明らかにメソッドfooBase()を継承しているため、それを呼び出すことができます。 2番目の例では、保護された関数を呼び出そうとしていますが、単純にそうです。どのコンテキストに関係なく、宣言されたクラスインスタンスから保護された関数を呼び出すことはできません。最初の例では、保護されたメソッドfooBaseを継承しているので、Fooコンテキスト内でそれを呼び出す権利があります。
私は物事をコンセプトやメッセージの観点から見る傾向があります。 FooBaseメソッドが実際に "SendMessage"と呼ばれ、Fooが "EnglishSpeakingPerson"で、FooBaseがSpeakingPersonである場合、protected宣言は、SendMessageをEnglishSpeakingPersons(およびサブクラス:たとえば、AmericanEnglishSpeakingPerson、AustralianEnglishSpeakingPerson)に制限することを目的としています。 SpeakingPersonから派生した別のタイプのFrenchSpeakingPersonは、FrenchSpeakingPersonをフレンドとして宣言しない限り、SendMessageを受信できません。「フレンド」とは、FrenchSpeakingPersonがEnglishSpeakingPersonからSendMessageを受信する特別な機能を持っている(つまり、英語を理解できる)ことを意味します。
hobo's answer に加えて、回避策を探すことができます。
サブクラスでfooBase
メソッドを呼び出したい場合は、static
にすることができます。静的保護メソッドには、すべての引数を持つサブクラスからアクセスできます。