ゲーム作りは楽しい

なんか書く

C++でコードベースのファイル名を使用したマイグレーションみたいなものを作った

コードベースのマイグレーションみたいなものを作りました。

wandbox.org

以下に

  • 1_Test.cpp
  • 2_Hoge.cpp

がありますが、これのprefixの数字をidとして、カレントバージョンから最新バージョンまで全部実行する仕組みです


1_Test.cpp

#include"Migration.hpp"
#include <iostream>

namespace 
{
    MIGRATION(Test)
    {
        void up() const override
        {
            std::cout << "Up Test" << std::endl;
        }
        void down() const override
        {
            std::cout << "Down Test" << std::endl;
        }
    }_;
}

2_Hoge.cpp

#include"Migration.hpp"
#include <iostream>

namespace 
{
    MIGRATION(Hoge)
    {
        void up() const override
        {
            std::cout << "Up Hoge" << std::endl;
        }
        void down() const override
        {
            std::cout << "Down Hoge" << std::endl;
        }
    }_;
}

呼び出し
main.cpp

int main()
{
    using Migration::MigrationHundler;
    int current = 0;
    std::cin >> current; 
    
    std::cout << "Latest: " << current << std::endl;
    
    std::cout << "-- Migration Up --" << std::endl;
    current = MigrationHundler::Up(current);
    std::cout << "Latest: " << current << std::endl;

    std::cout << "-- Migration Down --" << std::endl;
    current = MigrationHundler::Down(current);    
    std::cout << "Latest: " << current << std::endl;    
    
    std::cout << "-- Migration Down --" << std::endl;
    current = MigrationHundler::Down(current);    
    std::cout << "Latest: " << current << std::endl;      
}

出力

Latest: -1
-- Migration Up --
Up Test
Up Hoge
Latest: 2
-- Migration Down --
Down Hoge
Latest: 1
-- Migration Down --
Down Test
Latest: -1

実装

Migration.hpp

#pragma once
#include <vector>
#include <algorithm>
#include <unordered_map>
#include <optional>

namespace Migration
{
    struct IMigration
    {
    public:
        virtual ~IMigration() = default;
        virtual void up() const = 0;
        virtual void down() const = 0;
    };

    template<int Id>
    struct Migration;

    class MigrationHundler
    {
    public:
        template<int Id>
        static void Regist(IMigration* migration)
        {
            m_migrations[Id] = migration;
        }

        static IMigration* Get(int version)
        {
            return m_migrations[version];
        }
        
        static int Up(int currentVersion)
        {
            int nextVersion = -1;
            std::vector<int> updateVersions;
            for (auto&& m : m_migrations) {
                if (m.first > currentVersion) {
                    updateVersions.push_back(m.first);
                    if(nextVersion < m.first) {
                        nextVersion = m.first;
                    }
                }
            }
            std::ranges::sort(updateVersions);
            for(int v : updateVersions) {
                m_migrations[v]->up();
            }
            return nextVersion == -1 ? currentVersion : nextVersion;
        }
        static int Down(int currentVersion)
        {
            if(m_migrations.find(currentVersion) != m_migrations.end()){
                m_migrations[currentVersion]->down();
            }
            int nextVersion = -1;
            for (auto&& m : m_migrations) {
                if (m.first < currentVersion) {
                    if(nextVersion < m.first) {
                        nextVersion = m.first;
                    }
                }
            }
            return nextVersion;
        }        
    private:
        MigrationHundler() = default;
        MigrationHundler(const MigrationHundler& other) = delete;
        void operator=(const MigrationHundler& other) = delete;
    private:
        inline static std::unordered_map<int, IMigration*> m_migrations;
    };

    template<int Id>
    struct Migration : public IMigration
    {
    public:
        Migration()
        {
            MigrationHundler::Regist<Id>(this);
        }
    };

    namespace detail
    {
        consteval int versionId(const char* path)
        {
            // filename取得
            const char* filename = path;
            while (*path) {
                if (*path++ == '\\') {
                    filename = path;
                }
            }
            // バージョン値取得
            const char* str = filename;
            int value = 0;
            while (char c = *str) {
                if (c <= '9' && c >= '0') {
                    value = (c - '0') + value * 10;
                } else {
                    break;
                }
                ++str;
            }
            return value;
        }
    }
}

#define MIGRATION(name) struct name :  ::Migration::Migration<::Migration::detail::versionId(__FILE__)>

ファイル名からprefixのidへの変換はdetail::versionIdでします。
これをMIGRATIONマクロに隠蔽することでよしなに紐づけてます。

Hundlerへの登録はインスタンス生成時にthisポインタを渡しますので、各実装をしたら一つどこかにインスタンスが必要です。
例では_の変数名で無名名前空間に生成しています

実際の運用について

実際にはカレントバージョンをどこかに永続化する必要があるので何度も更新が走らないようにtxtなりdbなり自由に保存しておきましょう。