C++ 継承とメンバー関数ポインター
-
09-06-2019 - |
質問
C++ では、メンバー関数ポインターを使用して、派生クラス (または基本) クラスのメンバーを指すことができますか?
編集:おそらく例が役立つでしょう。3 つのクラスの階層があるとします。 X
, Y
, Z
継承順に。Y
したがって基本クラスがあります X
そして派生クラス Z
.
これで、メンバー関数ポインターを定義できるようになりました。 p
授業のために Y
. 。これは次のように書かれます。
void (Y::*p)();
(簡単にするために、シグネチャを持つ関数のみに興味があると仮定します。 void f()
)
このポインター p
クラスのメンバー関数を指すために使用できるようになりました Y
.
この質問 (実際には 2 つの質問) は次のようになります。
- できる
p
派生クラス内の関数を指すために使用されますZ
? - できる
p
基本クラス内の関数を指すために使用されますX
?
解決
C++03標準、 §4.11 2 メンバーへのポインタ変換:
型の右辺値「型 B のメンバーへのポインター」 履歴書 T」(B はクラス型)は、「型 D のメンバーへのポインタ」型の右辺値に変換できます。 履歴書 T」、ここで D は B の派生クラス (条項 10) です。B が D のアクセス不能 (11 節)、あいまい (10.2) または仮想 (10.1) 基底クラスである場合、この変換を必要とするプログラムは不正な形式です。変換の結果は、変換が行われる前のメンバーへのポインターと同じメンバーを参照しますが、基本クラスのメンバーを派生クラスのメンバーであるかのように参照します。結果は、B の D のインスタンスのメンバーを参照します。結果の型は「型の D のメンバーへのポインタ」であるため、 履歴書 T」は、D オブジェクトを使用して逆参照できます。結果は、B のメンバーへのポインターが D の B サブオブジェクトで逆参照された場合と同じになります。null メンバー ポインター値は、宛先型の null メンバー ポインター値に変換されます。 52)
52)メンバーへのポインターの変換の規則 (基底のメンバーへのポインターから派生のメンバーへのポインターへ) は、オブジェクトへのポインターの規則 (派生へのポインターから基底へのポインターへ) と比較して反転されているように見えます (4.10、節 10)。この反転は、型の安全性を確保するために必要です。メンバーへのポインターはオブジェクトへのポインターや関数へのポインターではなく、そのようなポインターの変換規則はメンバーへのポインターには適用されないことに注意してください。特に、メンバーへのポインターは void* に変換できません。
つまり、メンバーがあいまいでない限り、アクセス可能な非仮想基本クラスのメンバーへのポインターを、派生クラスのメンバーへのポインターに変換できます。
class A {
public:
void foo();
};
class B : public A {};
class C {
public:
void bar();
};
class D {
public:
void baz();
};
class E : public A, public B, private C, public virtual D {
public:
typedef void (E::*member)();
};
class F:public E {
public:
void bam();
};
...
int main() {
E::member mbr;
mbr = &A::foo; // invalid: ambiguous; E's A or B's A?
mbr = &C::bar; // invalid: C is private
mbr = &D::baz; // invalid: D is virtual
mbr = &F::bam; // invalid: conversion isn't defined by the standard
...
逆方向への変換 (経由) static_cast
) によって管理されます § 5.2.9 9:
型の右辺値「型の D のメンバーへのポインタ」 CV1 T" は、"型の B のメンバーへのポインター" 型の右辺値に変換できます。 CV2 T"、B は基本クラス (句) 10 クラス.派生) の D (「型 T の B のメンバーへのポインター」から「型 T の D のメンバーへのポインター」への有効な標準変換が存在する場合 (4.11 変換メモリ)、 そして CV2 と同じCV-qualification、またはそれよりも優れたCV-qualificationです。 CV1.11) null メンバー ポインター値 (4.11 変換メモリ) は、宛先型の null メンバー ポインター値に変換されます。クラス B に元のメンバーが含まれる場合、または元のメンバーを含むクラスの基本クラスまたは派生クラスである場合、結果として得られるメンバーへのポインターは元のメンバーを指します。それ以外の場合、キャストの結果は未定義です。[注記:クラス B には元のメンバーが含まれている必要はありませんが、メンバーへのポインターが逆参照されるオブジェクトの動的型には元のメンバーが含まれている必要があります。見る 5.5 expr.mptr.oper.]
11) 関数タイプ(メンバー関数タイプへのポインターで使用されるものを含む)は、CV資格を取得することはありません。見る 8.3.5 dcl.fct.
つまり、派生したものから変換できます。 D::*
基地へ B::*
から変換できれば B::*
に D::*
, ただし、使用できるのは B::*
タイプ D のオブジェクト、または D から派生したオブジェクト。
他のヒント
あなたが何を質問しているのかは 100% わかりませんが、仮想関数で動作する例を次に示します。
#include <iostream>
using namespace std;
class A {
public:
virtual void foo() { cout << "A::foo\n"; }
};
class B : public A {
public:
virtual void foo() { cout << "B::foo\n"; }
};
int main()
{
void (A::*bar)() = &A::foo;
(A().*bar)();
(B().*bar)();
return 0;
}
メンバーへのポインターに関する重要な問題は、それらが正しい型のクラスへの任意の参照またはポインターに適用できることです。これが意味するのは、 Z
から派生した Y
へのポインタ (または参照) 型のポインタ (または参照) Y
実際には、次の基本クラスのサブオブジェクトを指す (または参照する) 可能性があります。 Z
または 他のクラス に由来する Y
.
void (Y::*p)() = &Z::z_fn; // illegal
これは、次のメンバーへのポインタに割り当てられたものはすべて、 Y
実際にはどれでも動作する必要があります Y
. 。のメンバーを指すことが許可された場合 Z
(そのメンバーではありませんでした) Y
) のメンバー関数を呼び出すことができます。 Z
実際にはそうではない何かについて Z
.
一方、メンバーへのポインタは、 Y
のメンバーも指します Z
(継承とは、 Z
にはそのベースのすべての属性とメソッドがあります) ポインターをメンバーに変換することは合法ですか? Y
のメンバーへのポインタへ Z
. 。これは本質的に安全です。
void (Y::*p)() = &Y::y_fn;
void (Z::*q)() = p; // legal and safe
この記事をチェックしてみるとよいかもしれません メンバー関数ポインターと可能な限り最速の C++ デリゲート 簡単に言うと、場合によっては「はい」と言えます。
そう信じる。関数ポインタはそれ自体を識別するためにシグネチャを使用するため、基本/派生動作はそれを呼び出したオブジェクトに依存します。
私の実験により次のことが分かりました。警告 - これは未定義の動作である可能性があります。誰かが決定的なリファレンスを提供してくれると助かります。
- これは機能しましたが、派生メンバー関数を割り当てるときにキャストが必要でした。
p
. - これも機能しましたが、逆参照するときに追加のキャストが必要でした。
p
.
私たちが本当に野心的だと感じているなら、次のように尋ねることができます p
無関係なクラスのメンバー関数を指すために使用できます。試したわけではありませんが、 ファストデリゲート dagorymの回答にリンクされているページは、それが可能であることを示唆しています。
結論として、この方法ではメンバー関数ポインターの使用を避けるようにします。次のような文章は自信を呼び起こしません。
メンバー関数ポインター間のキャストは非常に暗いエリアです。C ++の標準化中、1つのクラスからベースまたは派生クラスのメンバー関数ポインターにメンバー関数ポインターをキャストできるはずであるかどうか、および無関係なクラス間でキャストできるかどうかについて多くの議論がありました。標準委員会が決心する頃には、さまざまなコンパイラベンダーがすでにこれらの質問に対する異なる答えに閉じ込められた実装決定を行っていました。[FastDelegate の記事]
あると仮定します class X, class Y : public X, and class Z : public Y
X と Y の両方のメソッドを void (Y::*p)() 型のポインターに割り当てることはできるはずですが、Z のメソッドは割り当てられません。その理由を確認するには、次の点を考慮してください。
void (Y::*p)() = &Z::func; // we pretend this is legal
Y * y = new Y; // clearly legal
(y->*p)(); // okay, follows the rules, but what would this mean?
この代入を許可することで、Y オブジェクトに対する Z のメソッドの呼び出しが許可され、誰にも分からない状態になる可能性があります。ポインタをキャストすることですべてを機能させることができますが、それは安全ではなく、動作する保証もありません。
以下にその例を示します。派生クラスのメソッドをオーバーライドすることができ、このオーバーライドされたメソッドへのポインターを使用する基本クラスの別のメソッドが実際に派生クラスのメソッドを呼び出します。
#include <iostream>
#include <string>
using namespace std;
class A {
public:
virtual void traverse(string arg) {
find(&A::visit, arg);
}
protected:
virtual void find(void (A::*method)(string arg), string arg) {
(this->*method)(arg);
}
virtual void visit(string arg) {
cout << "A::visit, arg:" << arg << endl;
}
};
class B : public A {
protected:
virtual void visit(string arg) {
cout << "B::visit, arg:" << arg << endl;
}
};
int main()
{
A a;
B b;
a.traverse("one");
b.traverse("two");
return 0;
}