🤔

【C++】純粋仮想関数と抽象クラス

に公開2

はじめに

今回のお話

今回は純粋C++で仮想関数(純粋仮想関数)と抽象クラスについてまとめたいと思います。

前提知識

1.仮想関数とは

親クラスで定義した関数を子クラスでも同じ関数名で定義することができる(オーバーライド)機能です.
C++では以下のようなキーワードを使います。(キーワードは省略可能ですが、私自身としては仮想関数であることを明示的に示した方が見やすいと思っている派なので記載します)

<親クラス・スーパークラス・基底クラス側(継承される側)> → virtual

Base.h
virtual void FunctionName();

<子クラス・サブクラス・派生クラス側(継承する側)> → virtual / override

Derived.h
virtual void FunctionName() override;

2.純粋仮想関数とは

純粋仮想関数は、親クラスの定義ができない継承した子クラスのみが関数の定義をすることができる機能です。
<私の考え(設計として正しくないかも)>
共通の処理はないけど、同じときに呼びたいとか同じ関数名でまとめておきたいときに使います。

C++では以下のように記載します。
<親クラス・スーパークラス・基底クラス側(継承される側)> → virtual

Base.h
virtual void FunctionName() = 0;

この 0 の意味は諸説あるみたいですが、一説によるとC言語の NULL からきている節もあったりします。

define.c
#define NULL 0
#define NULL (void*)0

3.オーバーライドさせないキーワード(Final)

Finalキーワードを使うことによってオーバーライドすることができなくなります.
<親クラス・スーパークラス・基底クラス側(継承される側)>

Base.h
virtual void UltimateFunctionName() final;

→この場合は,virtual にする必要性(ry

<子クラス・サブクラス・派生クラス側(継承する側)>

Derived.h
virtual void UltimateFunctionName() override final;

ちょっと実践

<親クラス・スーパークラス・基底クラス>

Base.h
#pragma once  // VSの書き方(clangでは使用不可)
#ifndef _BASE_H_
#define _BASE_H_

// 親クラス
class Base
{
public:
    Base();
    ~Base();
    
    virtual void BeginPlay();
    virtual void Tick(float DeltaTime);
    
    virtual void PureVirtual() = 0;
    virtual void ConstFunc() const = 0;
    
    virtual void FinalFunc() final;
};

#endif 

Base.cpp
#include "Base.h"
#include <iostream>

Base::Base()
{
    std::cout << "Base class Constructor" << std::endl;
}

Base::~Base()
{
    std::cout << "Base class Destructor" << std::endl;
}

void Base::BeginPlay()
{
    std::cout << "Base class:BeginPlay" << std::endl;
}

void Base::Tick(float DeltaTime)
{
    std::cout << "Base class:Tick" << std::endl;
}

void Base::FinalFunc()
{
    std::cout << "Base class:FinalFunc" << std::endl;
}

<子クラス・サブクラス・派生クラス>

Derived.h
#ifndef _DERIVED_H_
#define _DERIVED_H_

#include "Base.h"

class Derived : public Base
{
public:
    Derived();
    ~Derived();
    
    virtual void BeginPlay() override;
    virtual void Tick(float DeltaTime) override;
    
    virtual void PureVirtual() override;
    virtual void ConstFunc() const override;
};

#endif 
Derived.cpp
#include "Derived.h"
#include <iostream>

Derived::Derived()
{
    std::cout << "Derived class:constructor" << std::endl;
}

Derived::~Derived()
{
    std::cout << "Derived class:Destructor" << std::endl;
}

void Derived::BeginPlay()
{
    // UEの書き方
    // Super::BeginPlay();
    
    // Vitual studioの書き方
    // __super::BeginPlay();
    
    // clang系
    Base::BeginPlay();
    std::cout << "Derived class:BeginPlay" << std::endl;
}

void Derived::Tick(float DeltaTime)
{
    Base::Tick(DeltaTime);
    std::cout << "Derived class:Tick" << std::endl;
}

void Derived::PureVirtual()
{
    std::cout << "Derived class:PureVirtual" << std::endl;
}

void Derived::ConstFunc() const
{
    std::cout << "Derived class:CosntFunc" << std::endl;
}

mainクラスで実践

main.cpp
#include <iostream>
#include <memory>
#include "Base.h"
#include "Derived.h"

int main()
{
    Derived* d1 = new Derived();
    d1->BeginPlay();
    
    // キャストは可能
    Base* b1 = dynamic_cast<Base*>(d1);
    b1->FinalFunc();
    
    // ポリモーフィズムも可能
    Base* b2 = new Derived();
    b2->FinalFunc();
    
    delete d1;
    delete b2;
}

結果がこちら

result
Base class Constructor
Derived class:constructor
Base class:BeginPlay
Derived class:BeginPlay
Base class:FinalFunc
Base class Constructor
Derived class:constructor
Base class:FinalFunc
Derived class:Destructor
Base class Destructor
Base class Destructor

終わりに

仮想関数については頻繁に使う割には、いまいち学習しないので少しでも知識を深めることができたのかな...(大学基礎レベルになりそう)

Discussion

齊藤敦志齊藤敦志

delete b2; としたときに Base::~Base だけが呼び出され、 Derived::~Derived が呼び出されていません。 b2 の静的型は Base* なのでそれが指している先は Base である思って解体しようとした結果ですが、実際のオブジェクトの型は Derived なのですから Derived::~Derived が呼び出されるべきです。 そのためには Base::~Base (デストラクタ) は仮想関数にしなければなりません。

もしも内容が空っぽの何もしないデストラクタであっても必要な措置です。 デストラクタは解体時にプログラマが望む処理をさせることが出来るというだけでなく、暗黙に様々な後始末もしているのでオブジェクトの型に合ったデストラクタが呼び出されないと破綻することが有りえるからです。

問題が明瞭に顕在化しないことも多く、コンパイラも問題として検出し難いのでプログラマが注意深く扱うしか仕方がないです。

yohhoyyohhoy

(本題からは外れた参考情報として)

この 0 の意味は諸説あるみたいですが、一説によるとC言語の NULL からきている節もあったりします。

C++言語の生みの親 Bjarne Stroustrup 氏によれば、C++言語をつくるときに「新規キーワードの導入を避けた」「CやC++では慣例的に 0 を "存在しない(not there)" ことの表現に用いる」からだそうです。後者の観点では、NULL由来という説も大きく外してはいないのかもですね。

他サイトStackOverflowでの質問と回答 Why is a pure virtual function initialized by 0? が参考になります。