LoginSignup
3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-11

初めに

 今回は後編です、なので前半を見ていない方は、前半を見てからこの記事を見て下さい。

 前回は「ポリモーフィズム」とstd::shared_ptr<void>を使った例を紹介しました。基本は前回の内容で管理していても問題ないですが、今はC++17...間も無くC++20に入ろうとしている時代です。なので、基本も大事ですが、どうせなら新しい方法も使いましょう!!
 というわけで、今回紹介するのはstd::variantを使用した方法を紹介しようと思います。

std::variant

 std::variantC++17以上から使えるクラスで、可能性がある型を全てテンプレートに記入します。するとあら不思議、テンプレートに書かれている型なら、自由に切り替えれるではありませんか! という感じのクラスで、詳細はリファレンスサイトを見て下さい。

 前回の例を基に作成しているので、そこだけ注意してください。それに加えて#include <variant>を追加しています。


定義

 定義部分は前回を見て下さい。一様、前半にも書いているのですが、イメージ図だけもう一度載せておきます。
 std::variantは何でも入るわけですが、オブジェクト管理の観点からみると、そのままだと扱いづらいので継承させています。
継承.png


構築

main.cpp
    using VariantType = std::variant<Kuri, Nokono, Wanwa, Kupa>; // 実際の変数宣言時にこれを書いていると長くなるので、usingで予め書いておきます。

    std::vector<std::vector<VariantType>> array_enemies;

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

        array_enemies.resize(static_cast<size_t>(Type::Max)); // 種類分だけ構築する

        // struct Kuri
        array_enemies[static_cast<size_t>(Type::Kuri)].resize(BuildNum, Kuri{}); // とりあえず、複数個確保(多少のコピーが発生しますが、初期化なので見ないことにしておきます...)

        // struct Nokono
        array_enemies[static_cast<size_t>(Type::Nokono)].resize(BuildNum, Nokono{}); // とりあえず、複数個確保

        // struct Wanwa
        array_enemies[static_cast<size_t>(Type::Wanwa)].resize(BuildNum, Wanwa{}); // とりあえず、複数個確保

        // struct Kupa
        array_enemies[static_cast<size_t>(Type::Kupa)].resize(BuildNum, Kupa{}); // とりあえず、複数個確保
    }

 オブジェクト管理の構築部分になっています。この例ではstd::variantオブジェクトを配列にし、その配列を2次元配列にして種類分要素数を増やします。この例だと、構築時にコピーが発生していますが、「array_enemies[static_cast<size_t>(Type::Kuri)].emplace_back().emplace<Kuri>();」とすれば直接構築できます。気になる人は気になるところ1

variant例1.png

更新

main.cpp

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

            for (VariantType& enemy : enemies)
            {
                // これで特定の方にアクセス可能
                auto* kuri{ std::get_if<static_cast<size_t>(Type::Kuri)>(&enemy) }; // 直接インデックスを指定して取得する方法
                // auto* kuri{ std::get_if<Kuri>(&enemy) }; // 型を指定して取得する方法

                // 保持している型
                if (kuri)
                {
                    kuri->kuri = 10;
                }
                // 保持していない型(つまりnullptrが返される)
                else
                {
                }
            }
        }

        // 全更新
        for (auto& enemies : array_enemies)
        {
            for (VariantType& enemy : enemies)
            {
                std::visit([](auto& enm) { enm.Update(); }, enemy);
            }
        }
    }

 ここではオブジェクト管理の更新などになっています。ここの注目点はやはり「std::get_if」「std::visit」でしょうか。
 std::get_ifは特定の型にアクセス可能とする関数で、型判別もおこなっています。std::variantの特徴なのですが、RTTIを使っておらず、高速な判別を可能としています。逆に言えば、動的に決定した特定の型へのアクセスは不可能です2。ちなみに、std::getでも可能で、その場合は型チェックをおこなわず、戻り値は型への参照になります。
 次に、std::visitですが、この関数が共通パラメータへのアクセスを可能にしています。中身は複雑な事をおこない、所謂「Visitorパターン」を実装しています。3
 この辺の説明はこのサイトリファレンスリファレンスが非常に分かり易いです。


おまけ

main.h
struct EnemyVisitor
{
    void operator()(Kuri& left, const Kuri& right)
    {
        left = right;
        std::cout << "両方クリです。" << std::endl;
    }
    void operator()(Kuri& left, const Nokono& right)
    {
        std::cout << "クリとノコノです。" << std::endl;
    }
    void operator()(Kuri& left, const Wanwa& right)
    {
        std::cout << "クリとワンワです。" << std::endl;
    }
    void operator()(Kuri& left, const Kupa& right)
    {
        std::cout << "クリとクパです。" << std::endl;
    }

    //------------------------------------------------------

    void operator()(Nokono& left, const Kuri& right)
    {
        std::cout << "ノコノとクリです。" << std::endl;
    }
    void operator()(Nokono& left, const Nokono& right)
    {
        left = right;
        std::cout << "両方ノコノです。" << std::endl;
    }
    void operator()(Nokono& left, const Wanwa& right)
    {
        std::cout << "ノコノとワンワです。" << std::endl;
    }
    void operator()(Nokono& left, const Kupa& right)
    {
        std::cout << "ノコノとクパです。" << std::endl;
    }

    //------------------------------------------------------

    void operator()(Wanwa& left, const Kuri& right)
    {
        std::cout << "ワンワとクリです。" << std::endl;
    }
    void operator()(Wanwa& left, const Nokono& right)
    {
        std::cout << "ワンワとノコノです。" << std::endl;
    }
    void operator()(Wanwa& left, const Wanwa& right)
    {
        left = right;
        std::cout << "両方ワンワです。" << std::endl;
    }
    void operator()(Wanwa& left, const Kupa& right)
    {
        std::cout << "ワンワとクパです。" << std::endl;
    }

    //------------------------------------------------------

    void operator()(Kupa& left, const Kuri& right)
    {
        std::cout << "クパとクリです。" << std::endl;
    }
    void operator()(Kupa& left, const Nokono& right)
    {
        std::cout << "クパとノコノです。" << std::endl;
    }
    void operator()(Kupa& left, const Wanwa& right)
    {
        std::cout << "クパとワンワです。" << std::endl;
    }
    void operator()(Kupa& left, const Kupa& right)
    {
        left = right;
        std::cout << "両方クパです。" << std::endl;
    }
};
main.cpp
    static const std::vector<VariantType> enemies_param{ Kuri{}, Nokono{}, Wanwa{}, Kupa{} }; // 適用したいエネミー達

    // 複数のvariantオブジェクトから呼び出す関数オーバーロードを決定する
    {
        int add_number;

        std::cout << "適用したい敵を選んでね!" << std::endl;
        std::cout << "1:クリ、2:ノコノ、3:ワンワ、4:クパ ⇒ ";
        std::cin >> add_number;

        // 特定の要素を見た目上、動的に増やしたい場合は下記のコードになります。(一例ですが)
        // array_enemies.at(add_number - 1).emplace_back(enemies_param[add_number - 1]);

        for (auto& enemies : array_enemies)
        {
            for (VariantType& enemy : enemies)
            {
                std::visit(EnemyVisitor{}, enemy, enemies_param[add_number - 1]); // 忘れずに-1をしておく
            }
        }
    }

 上記のコードは2つのstd::variantを使用した、std::visit関数の例です。実行すれば分かるのですが、main.hstruct EnemyVisitoroperator()()による、見た目上の動的な型切り替えを可能にしています。ゲーム製作で使いどころは余り思いつきませんが「std::visitでこういう事も出来るよ」という感じで覚えておいて損はないと思います。
 ただし、想定しうる全てのパターンをオーバーロードとして定義していなければなりません。4 なので、struct EnemyVisitoroperator()()を一つでも削除するとエラーを吐きます。
 つまり、種類が増えれば増えるほどパターンが増えていく上に、同じ型同士でないと使う機会はそうそう無いと思うので、ほとんどの定義の中身が「無い」になりかねず、プログラマにとっては気持ち悪いコードかもしれません。


ここからは少し別の話題です

std::any

 これはオブジェクト管理とは関係ないのですが、せっかくなので紹介したいと思います。
 std::anyとは、名前の通りどの型でも入れられるクラスで、C++17以上で使えます。この時点で「あれ?」と思った方はすごいです。そうです、前回紹介したstd::shared_ptr<void>と機能が同じです。では違いは何かというと、リファレンスよれば

このクラスと同様のことは、たとえばstd::shared_ptr<void>でも行えるが、その場合はポインタの意味論で値を保持することになり、anyの場合は値の意味論で値を保持することになる。

となっています。結局、実装の判断はプログラマ次第ということです。あと、詳細はいつも通りリファレンスを見て下さい。

 上記に書いていますが、std::anyを使ってオブジェクト管理はかなり無理があるので、オブジェクト管理の観点からだと使うことはまずありません。ですが、この記事に書いてしまったので同じ例で紹介します。

定義

 定義部分は前回と同じです。それに加えて#include <any>を追加しています。

実装部分

main.cpp
    std::vector<std::vector<std::any>> array_enemies;

    /* 
        構築は同じなので省略します。
  */

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

            for (auto& enemy : enemies)
            {
                // 例外送出による型チェック
                {
                    Kuri kuri{};

                    // これで特定の型にアクセス可能
                    try
                    {
                        kuri = std::any_cast<Kuri>(enemy); // 明確な型を指定(コピーが発生)
                        // kuri = std::any_cast<Kuri&>(enemy); // こうすると参照が戻り値になる
                    }
                    catch (const std::bad_any_cast& bac)
                    {
                        // 型の指定を間違うと、bad_any_cast例外が送出される
                        std::cout << bac.what() << std::endl;
                    }

                    kuri.kuri = 10;
                }

                // ポインタによる型チェック
                {
                    auto* kuri{ std::any_cast<Kuri>(&enemy) }; // 明確な型を指定(ポインタなのでコピーについては無視できる)

                    // 保持している値へのポインタを取り出す
                    if (kuri)
                    {
                        kuri->kuri = 10;
                    }
                    // 型の指定を間違うとnullptrが返る
                    else
                    {
                    }
                }
            }
        }

        // 全更新
        for (auto& enemies : array_enemies)
        {
            for (auto& enemy : enemies)
            {
                // この書き方だと例外を出すので、std::shared_ptr<void>の代わりにはなりません。
                // 暗黙的にキャストされうる型であっても、直接の型を指定しなければならない仕様の為です。
                //std::any_cast<Base>(&enemy)->Update();
            }
        }
    }

 といった感じになります。上記の「全更新」の部分なのですが、直接の型を指定しなければならない為にstd::shaerd_ptr<void>の代わりにはなれません。if文やswitch文で出来なくはないですが、そんな使い方するぐらいなら、ほかの用途に使った方が絶対幸せなので、素直にstd::shared_ptr<void>などを使いましょう。
 間違えて「オブジェクト管理の手段として使える」と勘違いしないように強調して書いています。

 取得の際(std::any_cast)はポインタで型チェックをおこなうか、確実に型が決定している場合のみコピーか参照での取得をした方いいかとは思います。因みにstd::anyに参照を代入しようと頑張っている記事があるので紹介します。

最後に

 以上、std::variantを紹介しました。私の中で、オブジェクト管理方法はstd::variantを使うことがかなり多いですね。理由はポリモーフィズムより、静的に決定するからです。勿論、静的に決定することはデメリットもありますが、C++である以上 このデメリットもあることも理解したうえで使うべきだと思っています。といっても、ポリモーフィズムも割と使いますし、結局プログラマ次第になってきてしまいます。5
 プラスαとしてstd::anyも紹介しましたが、正直なところ自分の環境では使う機会はほとんどありませんでした。ですが、自分としてはstd::shared_ptr<void>よりもstd::anyの方が好きですね。

因みに、この記事のサブテーマは「C++17の環境で皆さんもプログラムしましょう!」です。


  1. といっても、これでは1つしか構築できないので、複数対応させようと思えば、for分などで回すしかないのでしょうか? 

  2. といっても、switch文やif文ですれば出来なくもありませんが...。 

  3. この関数の引数の並び順が、Algorithmなどの関数と比べると逆なので、凄い違和感があるのですが気のせいでしょうか? 

  4. こうなる理由はパターン決定をコンパイル時に決定する為。つまり静的に決定するからですね。 

  5. それを言ってしまっては終わり(笑) 

3
2
0

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
3
2