LoginSignup
5
6

More than 3 years have passed since last update.

複数のオブジェクトをどう管理するか 【前編】

Last updated at Posted at 2020-08-28

最初に

 前置として、この記事はC++初心者を対象にしています。中上級者が見ても物足りない可能性があるので注意して下さい。ただ初心者と言っても、C++プログラムの初心者ではなく、どちらかというとクラス設計の話になるので、そういう意味では中級者対象になるかもしれません。C++プログラム自体の初心者は他の記事を見て頑張って勉強して下さい1
 さて、前置きは置いておきまして、この記事はタイトル通りC++における「複数オブジェクト管理」をテーマとしています。このテーマは初心者なら誰でも躓く点ではないのでしょうか?2 そんな人達のためにこの記事を書くことにしました。と言ってもこの記事だけが正しいわけではないので、参考程度に留めておいて下さい。

後半を上げたので、気になる方は見て下さい。

オブジェクトをどう管理するか

 早速この記事の本題です。例をあげた方が分かりやすいと思うので、例でやります。
 

管理の必要性

「クリ・ノコノの2種類の敵を用意してくれ。ただ、動きは別々で。」とプランナーから注文を受けたとします。皆さんはどうしますか? 「別々のオブジェクトとして、別々に管理」しますか?それとも他の方法で管理しますか?
 「別々のオブジェクトとして、別々に管理」する場合は敵が2種類だけなら問題ないでしょう。

ダメな例.png


 後日「ごめん、ワンワ・クパも追加で‼︎」とプランナーが言い放ちました。するとどうでしょう。とりあえずプランナーにグーを放ち上記の方法で管理するとしたら、合計4つ目の変数が出来ますね。このままでは種類が増えると、変数が増えて管理がめんどくさいことになります。
 という様に、上記の方法では変数が敵の種類の数だけ増えることになり、管理しきれません。

別々に管理2.png
 なので、「別々に管理」ではなく「まとめて管理」をしたいと思いませんか?

管理方法

 今回の記事では一部を除きC++11で使えますが、今後の為にもC++17乗り換えましょう(`・ω・´)

下準備

 上記の例では、まず座標・角度・速度などの情報、更新・描画などの関数は共通化出来るので継承するとしましょう。そして、派生クラスに少し変数を追加しましょう。
別々に管理.png

ポリモーフィズム

 そして、C++と継承がくれば、ポリモーフィズムが真っ先に思いつきます。※他の言語は知りませんが(笑)
え?ポリモーフィズムって何?それはこの記事を見て下さい。一言で言うなら、「基底クラスを基にして、派生クラスを付け替える」と言う感じでしょうか。もっと知りたい人は各自調べて下さい(宿題です)
 兎に角、C++を学び始めて最初に学ぶオブジェクト管理方法はこの方法でしょう。
ポリモーフィズム.png

 ポリモーフィズム自体の例などは他の記事でいくらでもあるので、ここでは、ポリモーフィズムを使ったオブジェクト管理の例をあげます。

mian.h
#include <iostream>
#include <memory>
#include <vector>
#include <DirectXMath.h>

struct Base // 基底クラス
{
    DirectX::XMFLOAT2 pos, speed, scale, angle;

    virtual void Update() = 0;
    virtual void Draw() = 0;
};

struct Kuri final /*final: これ以上派生しない事を表す指定子*/
    : public Base
{
    int kuri;
    void Update() override /*override: 基底クラスの純粋仮想関数をオーバーライドする事を示す指定子*/
    {
        std::cout << "struct Kuri Update Func" << std::endl;
    }
    void Draw() override
    {
        std::cout << "struct Kuri Draw Func" << std::endl;
    }
};

struct Nokono final
    : public Base
{
    int nokono;
    void Update() override
    {
        std::cout << "struct Nokono Update Func" << std::endl;
    }
    void Draw() override
    {
        std::cout << "struct Nokono Draw Func" << std::endl;
    }
};

struct Wanwa final
    : public Base
{
    int wanwa;
    void Update() override
    {
        std::cout << "struct Wanwa Update Func" << std::endl;
    }
    void Draw() override
    {
        std::cout << "struct Wanwa Draw Func" << std::endl;
    }
};

struct Kupa final
    : public Base
{
    int kupa;
    void Update() override
    {
        std::cout << "struct Kupa Update Func" << std::endl;
    }
    void Draw() override
    {
        std::cout << "struct Kupa Draw Func" << std::endl;
    }
};

enum class Type // C++ならenumではなくて、enum class を使った方がいいです。
{
    Kuri,
    Nokono,
    Wanwa,
    Kupa,
};

 定義部分になっています。「final・override」はC++11以降使える指定子となっています。詳細はここを見て下さい。


main.cpp
std::vector<std::vector<std::shared_ptr<Base>>> array_enemies;

// 構築
{
    constexpr size_t BuildNum{ 10u }; // 仮構築数

    // struct Kuri
    {
        auto& enemies{ array_enemies.emplace_back() }; // emplace_back()に戻り値があるのはC++17以上からになります。

        enemies.resize(BuildNum); // とりあえず、複数個確保

        for (auto& enemy : enemies)
        {
            enemy = std::make_shared<Kuri>(); // 派生クラスを構築
        }
    }

    // struct Nokono
    {
        auto& enemies{ array_enemies.emplace_back() };

        enemies.resize(BuildNum); // とりあえず、複数個確保

        for (auto& enemy : enemies)
        {
            enemy = std::make_shared<Nokono>(); // 派生クラスを構築
        }
    }

    // struct Wanwa
    {
        auto& enemies{ array_enemies.emplace_back() };

        enemies.resize(BuildNum); // とりあえず、複数個確保

        for (auto& enemy : enemies)
        {
            enemy = std::make_shared<Wanwa>(); // 派生クラスを構築
        }
    }

    // struct Kupa
    {
        auto& enemies{ array_enemies.emplace_back() };

        enemies.resize(BuildNum); // とりあえず、複数個確保

        for (auto& enemy : enemies)
        {
            enemy = std::make_shared<Kupa>(); // 派生クラスを構築
        }
    }
}

 オブジェクト管理の構築部分になっています。ここの注意点は「生ポインタを使うのではなくスマートポインタを使いましょう」です。deleteし忘れて、メモリーリークするの嫌ですし3


main.cpp
// 更新
{
    // 特定のクラスを更新
    {
        auto& enemies{ array_enemies[static_cast<size_t>(Type::Kuri)] };

        for (auto& enemy : enemies)
        {
            auto& kuri{ std::dynamic_pointer_cast<Kuri>(enemy) }; // 重要!!

            assert(kuri && "ダウンキャスト失敗");

            kuri->kuri = 10;
        }
    }

    // 全更新
    for (auto& enemies : array_enemies)
    {
        for (auto& enemy : enemies)
        {
            enemy->Update();
        }
    }
}

// 描画
for (auto& enemies : array_enemies)
{
    for (auto& enemy : enemies)
    {
        enemy->Draw();
    }
}

 オブジェクト管理の更新などになっています。ここの注意点は「特定のクラスを更新」で、auto& kuri{ std::dynamic_pointer_cast<Kuri>(enemy) };を使ったダウンキャストする必要があるところです。std::dynamic_pointer_castで失敗した際は、nullptrのstd::shared_ptrが返ります4
 といっても、この例ではstruct Kuriだと確定しているので、std::static_pointer_cast<Kuri>(enemy)としても問題ないとは思います。

void* (std::shared_ptr<void>)

次に、void*を使った方法を紹介します。といっても、void*の知識としては

void *型は汎用ポインタと呼ばれ、あらゆるポインタを代入することができます。
https://ota42y.com/blog/2014/11/06/cpp-void-pointer/

の知識さえあれば使う分には問題ありません。もっと知りたいという人は各自調べて下さい1
shared_ptr.png

 例を紹介するんですが、内容的にはポリモーフィズムとあまり変わらないのですが...。ポリモーフィズムと比べてstd::shared_ptr<void>の利点としては、全く同じ別の基底クラス(えぇ...!?)を使った派生クラスも突っ込める等ぐらいでしょうか。他は...知りません...。(;^ω^)

 紹介します とほざきながら、自分はこの手段をほぼほぼ使ったことがないので「使い方おかしいだろ」ポイントが存在する可能性があるのは申し訳ありません。

main.h
/*
  定義部分は同じなので飛ばします
*/
main.cpp
    std::vector<std::vector<std::shared_ptr<void>>> array_enemies;

    /*
       構築部分は同じなので飛ばします
    */

    // 更新
    {
        // 特定のクラスを更新
        {
            auto& enemies{ array_enemies[static_cast<size_t>(Type::Kuri)] };

            for (auto& enemy : enemies)
            {
                auto& kuri{ std::static_pointer_cast<Kuri>(enemy) }; // 重要!!

                kuri->kuri = 10;
            }
        }

        // 全更新
        for (auto& enemies : array_enemies)
        {
            for (auto& enemy : enemies)
            {
                std::static_pointer_cast<Base>(enemy)->Update();
            }
        }
    }

    // 描画
    for (auto& enemies : array_enemies)
    {
        for (auto& enemy : enemies)
        {
            std::static_pointer_cast<Base>(enemy)->Draw();
        }
    }

 といった感じになります。注意点としては「std::static_pointer_cast」を使って特定のクラスにアクセスしているところと、「全更新・描画」の際にstd::static_pointer_cast<Base>(enemy)を使ってstruct Baseの要素にアクセスしているところです。
 特定のクラスにアクセスする際に、ポリモーフィズムと同じように「std::dynamic_pointer_cast」を使おうとすると、エラーを吐きます。ロベールさんを見ると

dynamic_cast では、void* へキャストすることもできます。しかし、void* からのキャストは行えません

と書いてあるので、「std::dynamic_pointer_cast」は使えないみたいです。つまり、動的な型変換は不可能ということなのでしょうか。
 といっても、「std::static_pointer_cast」と書いてある通り、処理速度は「std::dynamic_pointer_cast」と比べるとかなり高速なので余り気にしなくてもいい気がします(本当か?)。

まとめ

 前編では2つの方法を使ったオブジェクト管理を紹介しましたが、どうでしょうか?初心者に役に立てれば幸いです。もう一度言っておきますが、「こんな方法もあるよ」程度に思ってもらえればOKです。
 後半のvoid* を使った方法は「使える」というだけで、オブジェクト管理という点では使いずらいのかも知れません。基本は前半のポリモーフィズムを使った方が無難だと思います。
 後編は、C++17の機能をふんだんに使った方法を紹介します5

今回、簡易的なクラス図を作成するのに使ったのは「Draw.io」というサイトです。すごく使いやすいのでお勧めです。


最近になればなるほど 記事が長めになりすぎていた気がするので、短めに書くようにしました。


  1. 投げやり とは言ってはいけません。 

  2. 少なくとも自分は躓きました。 

  3. 自分は生ポインタは絶対に使いたくない症なので、特殊なケースでない限りはスマートポインタを使います。 

  4. dynamic_cast自体がRTTIになってくるので詳しい説明はしません。RTTIは便利なんですが、速度がネックになってきます。やっぱりC++は静的に型を決定する言語だと勝手に思っています。 

  5. これを機にC++17環境にしましょう!! 

5
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6