サーチ…
多相クラスを定義する
典型的な例は、抽象的な形状クラスであり、正方形、円、および他のコンクリート形状に導き出すことができます。
親クラス:
ポリモーフィッククラスから始めましょう:
class Shape {
public:
virtual ~Shape() = default;
virtual double get_surface() const = 0;
virtual void describe_object() const { std::cout << "this is a shape" << std::endl; }
double get_doubled_surface() const { return 2 * get_surface(); }
};
この定義を読むには?
virtual
のキーワードを使用して、導入されたメンバー関数によって多態的な振る舞いを定義することができます。ここで、get_surface()
とdescribe_object()
は、円の場合よりも正方形の場合には別々に実装されることは明らかです。関数がオブジェクトに対して呼び出されると、オブジェクトの実クラスに対応する関数が実行時に決定されます。抽象シェイプの
get_surface()
を定義するのは意味がありません。これは関数の後に= 0
が続く理由です。これは、関数が純粋な仮想関数であることを意味します 。多相クラスは、常に仮想デストラクタを定義する必要があります。
非仮想メンバ関数を定義することができます。これらの関数がオブジェクトに対して呼び出されると、関数はコンパイル時に使用されるクラスに応じて選択されます。ここで
get_double_surface()
はこのように定義されています。少なくとも1つの純粋仮想関数を含むクラスは、抽象クラスです。抽象クラスはインスタンス化できません。ポインターや抽象クラス型の参照のみを持つことができます。
派生クラス
多相基本クラスが定義されると、それを派生させることができます。例えば:
class Square : public Shape {
Point top_left;
double side_length;
public:
Square (const Point& top_left, double side)
: top_left(top_left), side_length(side_length) {}
double get_surface() override { return side_length * side_length; }
void describe_object() override {
std::cout << "this is a square starting at " << top_left.x << ", " << top_left.y
<< " with a length of " << side_length << std::endl;
}
};
いくつかの説明:
- 親クラスの任意の仮想関数を定義または上書きできます。ファンクションが親クラスで仮想であったという事実は、それを派生クラスで仮想化します。コンパイラに再びキーワード
virtual
を伝える必要はありません。しかし、関数宣言の最後にキーワードoverride
を追加することをお勧めしoverride
。これは、関数のシグネチャが気付かれていないことによる微妙なバグを防ぐためです。 - 親クラスのすべての純粋仮想関数が定義されている場合は、このクラスのオブジェクトをインスタンス化することができます。そうでなければ抽象クラスにもなります。
- すべての仮想関数をオーバーライドする義務はありません。必要に応じて、親のバージョンを保持することができます。
インスタンス化の例
int main() {
Square square(Point(10.0, 0.0), 6); // we know it's a square, the compiler also
square.describe_object();
std::cout << "Surface: " << square.get_surface() << std::endl;
Circle circle(Point(0.0, 0.0), 5);
Shape *ps = nullptr; // we don't know yet the real type of the object
ps = &circle; // it's a circle, but it could as well be a square
ps->describe_object();
std::cout << "Surface: " << ps->get_surface() << std::endl;
}
安全なダウンキャスティング
ポリモーフィッククラスのオブジェクトへのポインタがあるとします。
Shape *ps; // see example on defining a polymorphic class
ps = get_a_new_random_shape(); // if you don't have such a function yet, you
// could just write ps = new Square(0.0,0.0, 5);
ダウンキャストは、一般的な多形のShape
から、 Square
やCircle
ようなより具体的な形にCircle
ます。
なぜダウンキャスト?
たいていの場合、仮想関数は型から独立してオブジェクトを操作することができるため、オブジェクトの実際の型を知る必要はありません。
std::cout << "Surface: " << ps->get_surface() << std::endl;
あなたがダウンキャストを必要としないなら、あなたのデザインは完璧です。
しかし、ダウンキャストする必要があるかもしれません。典型的な例は、子クラスに対してのみ存在する非仮想関数を呼び出す場合です。
たとえば円を考えてみましょう。円だけが直径を持っています。クラスは次のように定義されます:
class Circle: public Shape { // for Shape, see example on defining a polymorphic class
Point center;
double radius;
public:
Circle (const Point& center, double radius)
: center(center), radius(radius) {}
double get_surface() const override { return r * r * M_PI; }
// this is only for circles. Makes no sense for other shapes
double get_diameter() const { return 2 * r; }
};
get_diameter()
メンバ関数は、円に対してのみ存在します。 Shape
オブジェクトに定義されていませんでした。
Shape* ps = get_any_shape();
ps->get_diameter(); // OUCH !!! Compilation error
どのようにダウンキャスト?
ps
がサークルを指していることが確実であれば、 static_cast
選択することができます:
std::cout << "Diameter: " << static_cast<Circle*>(ps)->get_diameter() << std::endl;
これはトリックを行います。しかし、それは非常に危険です: ps
がCircle
以外の何かによって見える場合、あなたのコードの振る舞いは未定義です。
だから、ロシアのルーレットを弾くのではなく、安全にdynamic_cast
使うべきです。これは特に多相クラスのためのものです:
int main() {
Circle circle(Point(0.0, 0.0), 10);
Shape &shape = circle;
std::cout << "The shape has a surface of " << shape.get_surface() << std::endl;
//shape.get_diameter(); // OUCH !!! Compilation error
Circle *pc = dynamic_cast<Circle*>(&shape); // will be nullptr if ps wasn't a circle
if (pc)
std::cout << "The shape is a circle of diameter " << pc->get_diameter() << std::endl;
else
std::cout << "The shape isn't a circle !" << std::endl;
}
多態性でないクラスでは、 dynamic_cast
は使用できません。クラスやその親に少なくとも1つの仮想関数が必要です。
多態性とデストラクタ
クラスが多態的に使用されることを意図されており、派生インスタンスがベースポインタ/参照として格納されている場合、その基本クラスのデストラクタはvirtual
かprotected
いる必要があります。前者の場合、これはオブジェクト破壊によってvtable
をチェックし、動的型に基づいて正しいデストラクタを自動的に呼び出します。後者の場合、基本クラスのポインタ/参照によってオブジェクトを破棄することはできません。オブジェクトは、実際の型として明示的に扱われた場合にのみ削除できます。
struct VirtualDestructor {
virtual ~VirtualDestructor() = default;
};
struct VirtualDerived : VirtualDestructor {};
struct ProtectedDestructor {
protected:
~ProtectedDestructor() = default;
};
struct ProtectedDerived : ProtectedDestructor {
~ProtectedDerived() = default;
};
// ...
VirtualDestructor* vd = new VirtualDerived;
delete vd; // Looks up VirtualDestructor::~VirtualDestructor() in vtable, sees it's
// VirtualDerived::~VirtualDerived(), calls that.
ProtectedDestructor* pd = new ProtectedDerived;
delete pd; // Error: ProtectedDestructor::~ProtectedDestructor() is protected.
delete static_cast<ProtectedDerived*>(pd); // Good.
これら両方の方法は、派生クラスのデストラクタが派生クラスインスタンスで常に呼び出され、メモリリークを防ぐことを保証します。