【C++】OOPとネイティブ開発の壁

DALL·E 2024-01-13 12.45.06-Photo-style-illustration C/C++
DALL·E 2024-01-13 12.45.06-Photo-style-illustration

こんにちは😃

今回からC++のゲーム開発に関する記事を投稿していきます٩(′ω′)و
このゲームは既存のゲームを二次創作としてWindows環境で構築するプロジェクトです。

以前の記事から度々転載しておりますが、ご容赦ください。

基本的にはC++でDXライブラリという専用の開発環境を利用して開発を行っています。

一時期はもっとネイティブなDirectXのオリジナルライブラリとWindows.h(Windows標準ヘッダー)を駆使してゲームを開発していましたが、それはあまりにも無駄な労力が割かれるという理由で断念しています。

DXライブラリはWindowsの2D・3Dゲームを作成するためにDirectXの関数を簡易的に利用できる形へ置き換えている利点を持ちながら、Windowsの各種ヘッダー関数を利用できるというC++プログラミングの属性を踏み外す事ない特性も維持している優秀な開発環境です。

DXライブラリ置き場 HOME

この開発環境は誰でも無料で利用する事ができます。


static_cast

C++言語に触れたのはおよそ8か月ぶりになるのですが、もう勝手が分からなくなってきてて脳の退化を感じ焦っております💦

C++はその立ち位置からオブジェクト指向言語でありながら、メモリ管理やダイレクトなメモリアクセスも容易にできる持ち味が特徴的で、使い方によっては実行時バグを伴うデータの破損に多くめぐり遭います。

例えば「依存関係の循環回避」や「部品の独立性確立」などがこの世界にはあり、これらを常に意識しながら設計を進めていく必要があります。

そんな事は私も分かっているわけではないので偉そうな事は言えないのですが、気にしてないわけではありませんし、今それは注視するべき内容だと理解しています(˘ω˘)

最近ではIDE(コンパイラなど)やテキストエディタが警告やエラーとしてソースコードの判定をしてくれるので、そのちょっとしたミスや落ち度に気づく事自体は比較的容易になりました。


static_castとは型変換を行うためのキャスト演算子であり、クラスではない特別な仕組みのことです。

例えば、

#include <iostream>

enum class Character
{
    虎杖悠仁,
    五条悟,
    釘崎野薔薇,
    夏油傑
};

int main()
{
    int c = Character::虎杖悠仁;
    std::cout << c << std::endl;
    return 0;
}

列挙型は配列を内部で自動的に数値として認識する特徴がありますが、「int c = …」の部分でそのまま代入できません。
このコードはコンパイル時にエラーとなります。

これがC++の厳密的な仕様の一例です。
ちなみにですが、

...
enum Character
{
    虎杖悠仁,
    五条悟,
    釘崎野薔薇,
    夏油傑
};
...

このように変更すれば「int c = …」の部分は暗黙の型変換をするようになります。
しかし大人の事情で暗黙の型変換は危険視される要因になっています。

C++の一般的な対応策として、通常の場合は以下のように修正します。

#include <iostream>

enum class Character
{
    虎杖悠仁,
    五条悟,
    釘崎野薔薇,
    夏油傑
};

int main()
{
    int c = static_cast<int>(Character::虎杖悠仁);
    std::cout << c << std::endl;
    return 0;
}
出力結果:
0

static_castは内部的に変換が可能なデータの場合にのみ動作する事が保証されます。
たとえば int ⇔ char は標準のC++内部では数値であるため、static_castを用いて型変換を行う事ができます。


今回私が遭遇した例は以下のコードです。

bool SoundLoader::checkNowPlaying(uint16_t channel) const {
    std::array<uint8_t, 5ULL> chList = { 1U, 2U, 4U, 8U, 16U };
    uint16_t index;
    for (const auto& e : chList) {
        index = &e - &chList[0];
        if ((channel & e) && (1 == DxLib::CheckSoundMem(_tunerHandle[index].SoundHandle))) return true;
    }
    return false;
}

これはクラスメソッドの一部ですが、このソースコードをビルドすると以下の警告が出力されました。

「warning C4244: ‘=’: ‘__int64’ から ‘uint16_t’ への変換です。データが失われる可能性があります。」

これが起きているのは「index = &e – &chList[0];」の部分でした。

では何がいけないのか。

上記は範囲for文(The range-based for statement)を使用して、chList配列の頭から順番にアクセスし、そのメモリ番地を計算していますね。
indexには、0→1→2・・・と繰り返し代入されますが、その数値が ‘__int64’型 となります。

‘__int64’型 ってなんですか?という話もあるのですが、integerに分類される数値型なのは変わりありません。

しかしそれに目を光らせるのがコンパイラのお家芸です。

実際には、’uint16_t’型 も ‘(unsigned)int’型 なのですがなぜわざわざそう書いているかと言うと、C++では ‘int’ キーワードを使用する事を必ずしも推奨していないからです。

話題がずれるのでこの話はしませんが、’int’ を使わず ‘int32_t’ や ‘uint32_t’ などを使うべきだと思います。1

「index = &e – &chList[0];」は、「&e – &chList[0]」の結果が ‘__int64’型 になっている事による型の不一致であり、ポインタの差を計算する式において起きるものです。

実際には ‘ptrdiff_t’型 というC++の型が使用されるのですが、これは文の都合上おきてしまうものです。
なのでここでは明示的に型変換(キャスト)をしてやる必要があるのです。

bool SoundLoader::checkNowPlaying(uint16_t channel) const {
    std::array<uint8_t, 5ULL> chList = { 1U, 2U, 4U, 8U, 16U };
    uint16_t index;
    for (const auto& e : chList) {
        index = static_cast<uint16_t>(&e - &chList[0]);
        if ((channel & e) && (1 == DxLib::CheckSoundMem(_tunerHandle[index].SoundHandle))) return true;
    }
    return false;
}

ここでstatic_castによる型変換をします。
変数indexは ‘uint16_t’ で定義してあるので、それと同じ型に揃えます。

これで先で説明した警告は出力されなくなりました。

C++ではこういった部分も厳密に対応していく必要があります。
なかなか大変ですが、神経質な言語はそのおかけで不具合の起こりにくいプログラムが構築できると言えるでしょう。

C++が組み込み系などでよく使われるのは、そういった側面がある事も理由として挙げられるかなと思います。


メモリリークについて

もう一つC++での開発中の悩みの種があり、これの対応が結構時間を取る要因になっています。

それがメモリリークです。

Visual Studio IDEでメモリの解放漏れをチェックしている様子

メモリリークはC++やC言語などで主に使用されるメモリアドレスのリザーブ機能を使用する事により発生する現象です。
いわゆるポインタの解放漏れですね。

それをしないとメモリはその後参照ができない使用中の状態で残存するという亡霊のようなものになり、PCのメモリ領域がその分なくなってしまいます。

なので基本的にはメモリリークは放置してはいけません。


これについては対応中です。

具体的には以下のように定義している箇所が危険因子です。

class ObjectDealer {
public:
    ObjectDealer() {}
    ~ObjectDealer() {}

    bool mapLoad(const std::wstring name, const std::wstring filepath, const LPDivGraphParam parameter);
    // ... その他のメソッド

private:
    std::vector<sound::IndexedScores> _sound;
    // ... その他のメンバ
};

struct IndexedScores {
    std::wstring index;
    IScore* value;

    IndexedScores(std::wstring idx, IScore* val) : index(idx), value(val) {}
};

class IScore {
public:
    virtual bool unzip() = 0;
    // ... その他の仮想メソッド
    virtual ~IScore() {}
};

上記の場合、IndexScores構造体(クラス)を使用している_soundメンバのアロケーテッドチェックをObjectDealerのデストラクタで行う必要があるでしょう。

IndexedScoresのメンバ「IScore」はポインタであるため、IScoreの具象インスタンスを格納した後、破棄している様子がないからです。

こうやって実際に因子が見つかるものはまだ良いのです。
問題なのは、このポインタをあちこちに渡して管理の目が行き届かない領域にまで出てしまったポインタです。

クラスの設計はそこで難しい側面を見せてくるのです。

単にポインタの解放処理を書くと言っても、コードを実装している時はメモリの確保が実質の実装コードなので、後始末の管理まで目が行き届かないのが現状なのです。

それはある種マネジメント工学にも一脈通じるところもある思想ではないかと思います。

そう考えるとなんだか難しいですね😅


また、C++(ISO/IEC 14882:2011標準規格以降バージョン)にはスマートポインタ2という概念があります。

この仕組みを利用すれば、従来よりもメモリアロケーションの管理の手間を省く事ができるため、環境が許す場合は積極的に取り入れたい概念ではありますね。

余談ですが、このプロジェクトを始めた当初まだ2010年くらいで最初はC言語で開発をしていました。

メモリ確保というとmallocを使うイメージが払しょくできていないのも要因としてあるのではないかなと思ってます😓

まだまだレガシー思考です・・・


オブジェクト指向設計をする

C#の記事でも何度か触れましたが、オブジェクト指向設計をしていきたいです。

オブジェクト指向言語だから。

C++には ‘interface’ キーワードや ‘abstract’ キーワードがないなど、他のOOP系言語には若干劣りますがクラス設計や継承、ポリモルフィズムなどの概念が備わっています。

プログラミング言語にオブジェクト指向の概念が眠っている場合、それを積極的に有効活用するべきです。

そしてデザインパターンやSOLID原則、KISS、命名の重要性など培ってきた技術も利用していきたいです。

設計原則とSOLIDについてのノート に非常に有益な情報が記載されていました。

プログラミングで重要な事は、シンプルで読めるコードを書く事だと思っています。
それは単独開発においても同じ事だと思っています。

以下はSOLID原則についての備忘です。
C#の記事からの引用になります。

  • Single Responsibility Principle : 単一責任の原則
    • SOLID原則の一つ。クラスなどのモジュールは一つの責任だけを担うようにしましょう、という意味です。
  • Open-Closed Principle : オープン・クローズドの原則
    • SOLID原則の一つで、これはコードの改訂を行う際に既存のコードを直接変更するのではなく、コードに追記するようにしましょうというものです。
  • Liskov Substitution Principle : リスコフの置換原則
    • これは少し概念が曖昧で難しいですが、若干緩い解釈を含めて言うと親のインターフェースに対して子どもであるクラスが無尽蔵に機能を覚えてどんどん変貌していき、本来の役割から逸脱した存在になってしまう現象を抑制するための規定の事です。一般的にインターフェースなどの抽象クラスは具象クラスとして利用する実装を定める役割を持っている必要があるため、このルールを守りましょう、という事です。is-a関係。
  • Interface Segregation Principle : インターフェース分離の原則
    • これもSOLID原則の一つです。さきほどのリスコフの置換原則と若干説明が関連しますが、インターフェースなどの抽象クラスが誇大化して、他の具象クラスから利用したくても不要な機能の実装を強要されてしまう問題を回避するためのものです。単一責任の原則にも通じる概念かと思います。
  • Dependency Inversion Principle : 依存関係逆転の原則
    • SOLID原則の中でも恐らく一番重要な(ものだと私は思っている)原則です。これはインターフェースとクラスは親子関係であくまでも子は親に依存すべきで、親が子に依存してはいけないという概念です。さきほどのTableLayoutPanelの話がこの概念の違反パターンです。インターフェースがある具象クラスに依存してしまっている場合、その具象クラスの仕様が変わるとインターフェースも修正しないといけなくなり、更にはそのインターフェースを継承・実装している他の具象クラス・・・それはもう考えたくない事態ですよね😅

サンプル

以下、サンプルで作成したものです。
現時点では土台を作っている最中なので、まだステージとかは実装できていません。

ゲームパッドのキー入力チェッカー
FC音源の発音テスター

なお、このプロジェクトのソースコードは下記のgithub上にて公開しています。

unlimitedloop-admin/enigma: This is a project to 100% reproduce the Rockman 2 (that hack game) with a Windows application.

もし興味のある方がおられましたら、ご自由にお持ちください😄

今回の記事はこれで終わりです。
次回の更新はロックマンの挙動にまつわるアルゴリズム周りを紹介できればと思っています。

  1. C++でクリーンなコードの書き方 #C++ – Qiita https://qiita.com/elipmoc101/items/01003c82dbd2e464a071#int%E5%9E%8B%E3%82%92%E4%BD%BF%E3%81%86%E3%81%AA ↩︎
  2. C++20スマートポインタ入門 #C++ – Qiita https://qiita.com/hmito/items/9b35a2438a8b8ee4b5af ↩︎

コメント

タイトルとURLをコピーしました