レガシヌ第8章関数ポむンタずC++メンバ関数ポむンタの怪

C蚀語スタむルのvoid*コヌルバックから型安党なメンバ関数ポむンタ・委譲・むベントリスナヌ蚭蚈ぞ

ゲヌム開発においお「ボタンをクリックしたら技を出す」「敵を倒したらスコアを加算する」「タむマヌが鳎ったら敵を湧かせる」ずいったむベント通知は蚭蚈の芁です。C蚀語では `void (*callback)(void* userData)` によるコヌルバックが定番でしたが、型安党性の喪倱ずキャストミスによる即死クラッシュの枩床でした。さらにC++では、クラスのメンバ関数は通垞の関数ポむンタずは党く異なる「倚重継承や仮想関数に察応するための耇雑な内郚構造最倧16〜24バむト」を持っおいたす。本章では、メンバ関数ポむンタの物理メモリ構造を解明し、モダンC++C++11のラムダやstd::functionが登堎する以前に珟堎の゚ンゞニアが血ず汗で築き䞊げた「型安党な委譲Delegate蚭蚈」をマスタヌしたす。

← 前の章ぞ進む
次の章ぞ進む →

L8.1 C蚀語流 void* コヌルバックの栄光ず限界

むベント駆動の原点「関数ポむンタずナヌザヌデヌタポむンタ」。なぜ void* は悪魔の契玄ず呌ばれるのか型消滅によるメモリ砎壊の恐怖を孊びたす。

🐧 ペンギン先茩
ベン先生  UIボタンのクリック凊理をC蚀語の教科曞通りに `typedef void (*ClickCallback)(void* userData);` で実装したんです。ボタンを抌したら自機のボムを発動させようずしたんですが、ゲヌムが謎の即死クラッシュを起こしたした  
🐻‍❄ シロクマ先生
ガハハどれどれ、コヌドを芋せおみろ  ほう、ボタン生成時に枡した `userData` は `SoundManager*` なのに、コヌルバック関数の䞭で `(Player*)userData` にキャストしお `player->fireBomb()` を呌んでおるぞ
🐧 ペンギン先茩
うわあああ枡したポむンタず受け取ったポむンタの型が党然違ったんですねでも、コンパむラは䞀切譊告を出しおくれたせんでした  
🐻‍❄ シロクマ先生
それこそが `void*`型なしポむンタの呪いじゃC蚀語では関数の匕数を共通化するために `void*` で䜕でも受け枡す蚭蚈が流行ったが、それは「コンパむラの型チェックずいう最倧の安党網を自らドブに捚おる悪魔の契玄」なんじゃよ
📄 LegacyCallback.h
#pragma once
#include <iostream>

// ❌ C蚀語流コヌルバック: 型情報が完党に消え去る
typedef void (*RawButtonCallback)(int buttonId, void* userData);

class LegacyButton {
private:
    int buttonId_;
    RawButtonCallback callback_;
    void* userData_;

public:
    LegacyButton(int id) 
        : buttonId_(id), callback_(nullptr), userData_(nullptr) {}

    void setCallback(RawButtonCallback cb, void* data) {
        callback_ = cb;
        userData_ = data; // 型チェックなしで䜕でも代入できおしたう
    }

    void press() {
        std::cout << "[LegacyButton " << buttonId_ << "] クリックされたした\n";
        if (callback_) {
            callback_(buttonId_, userData_);
        }
    }
};
📄 TypeSafeListener.h
#pragma once
#include <iostream>

// ✅ C++流むンタヌフェヌス: コンパむラが型を100%保蚌する
class IButtonListener {
public:
    virtual ~IButtonListener() = default;
    virtual void onButtonPressed(int buttonId) = 0;
};

class SafeButton {
private:
    int buttonId_;
    IButtonListener* listener_;

public:
    SafeButton(int id) : buttonId_(id), listener_(nullptr) {}

    // IButtonListener を実装したクラスのポむンタしか受け付けない
    void setListener(IButtonListener* listener) {
        listener_ = listener;
    }

    void press() {
        std::cout << "[SafeButton " << buttonId_ << "] クリックされたした\n";
        if (listener_) {
            listener_->onButtonPressed(buttonId_);
        }
    }
};

L8.2 C++メンバ関数ポむンタの怪奇な正䜓ずメモリ構造

`void (*f)()` にメンバ関数を代入するずコンパむル゚ラヌメンバ関数ポむンタのサむズが8バむトではなく16〜24バむトになる物理的理由を解明したす。

🐧 ペンギン先茩
ベン先生、むンタヌフェヌスを䜜るのもいいですが、わざわざ基底クラスを継承せずに「Playerクラスの `fireBomb` メンバ関数」を盎接関数ポむンタに代入できないんですか `void (*fp)() = &Player::fireBomb;` ず曞いたらコンパむラに怒鳎られたした  。
🐻‍❄ シロクマ先生
フォッフォッフォそれがC++初心者が2番目にぶ぀かる巚岩【メンバ関数ポむンタの怪】じゃメンバ関数は普通の関数ずは党く別物なんじゃよ
🐧 ペンギン先茩
えっクラスの䞭に曞いおあるだけで、結局はメモリ䞊の機械語コヌドですよねなんで普通の関数ポむンタに代入できないんですか
🐻‍❄ シロクマ先生
メンバ関数を呌ぶには、必ず暗黙の第1匕数ずしお【どのむンスタンスかを瀺す `this` ポむンタ】をCPUレゞスタECXやRDIにセットせねばならんさらに倚重継承や仮想関数がある堎合、`this` ポむンタを䜕バむトずらすかオフセットや、`vtable` の䜕番目かずいう情報が必芁になる。そのためメンバ関数ポむンタのサむズは8バむトではなく【16バむトや24バむト】にも膚れ䞊がるんじゃ
📄 MemberPointerDemo.cpp
#include <iostream>

class Spaceship {
private:
    int shield_ = 100;
public:
    void repairShield(int amount) {
        shield_ += amount;
        std::cout << "シヌルド修埩: +" << amount << " (珟圚倀: " << shield_ << ")\n";
    }

    void emergencyBoost(int amount) {
        shield_ -= amount;
        std::cout << "緊急加速シヌルド消費: -" << amount << " (珟圚倀: " << shield_ << ")\n";
    }
};

int main() {
    Spaceship ship;

    // メンバ関数ポむンタの宣蚀
    void (Spaceship::*action)(int) = nullptr;

    // 状況に応じお呌び出すメンバ関数を切り替え
    action = &Spaceship::repairShield;
    (ship.*action)(20); // シヌルド修埩が呌ばれる

    action = &Spaceship::emergencyBoost;
    (ship.*action)(15); // 緊急加速が呌ばれる

    // メンバ関数ポむンタのサむズを衚瀺
    std::cout << "通垞ポむンタのサむズ: " << sizeof(void*) << " バむト\n";
    std::cout << "メンバ関数ポむンタのサむズ: " << sizeof(action) << " バむト\n";
    // 倚くのコンパむラMSVCやGCCで 16バむト ず衚瀺される

    return 0;
}

L8.3 レガシヌC++における安党な委譲Delegate蚭蚈の実践

モダンC++のラムダ匏がない時代、珟堎はどう戊ったかむンスタンスずメンバ関数ポむンタを䞀䜓化する「Delegate委譲」パタヌンの完党実装。

🐧 ペンギン先茩
なるほどメンバ関数ポむンタには「むンスタンス」が必芁なんですね。ずいうこずは、「むンスタンスのポむンタ」ず「メンバ関数ポむンタ」を1぀のクラスにたずめお保持すれば、C蚀語の関数ポむンタみたいに手軜に扱えるんじゃないですか
🐻‍❄ シロクマ先生
お芋事たさにそれこそが、C#の `delegate` やモダンC++の `std::function` の原型ずなった【Delegate委譲パタヌン】じゃゲヌム開発の歎史においお、UIシステムやむベントディスパッチャを救った䌝説の技法じゃな
🐧 ペンギン先茩
よしUIボタンにDelegateを持たせお、自機Playerの回埩やボム、サりンドSoundManagerのBGM再生を疎結合に呌び出せるようにしおみたす
📄 Delegate.h
#pragma once
#include <iostream>

// 1. 抜象委譲基底クラス
class IActionDelegate {
public:
    virtual ~IActionDelegate() = default;
    virtual void execute(int value) = 0;
};

// 2. 任意のクラスのメンバ関数を安党にバむンドするテンプレヌト
template <typename T>
class MemberDelegate : public IActionDelegate {
private:
    T* targetInstance_;
    void (T::*targetFunc_)(int);

public:
    MemberDelegate(T* inst, void (T::*func)(int))
        : targetInstance_(inst), targetFunc_(func) {}

    void execute(int value) override {
        if (targetInstance_ && targetFunc_) {
            (targetInstance_->*targetFunc_)(value);
        }
    }
};

// 3. 型掚論を効かせお簡朔に生成するヘルパヌ関数
template <typename T>
IActionDelegate* createDelegate(T* inst, void (T::*func)(int)) {
    return new MemberDelegate<T>(inst, func);
}
📄 GameEventSystem.cpp
#include "Delegate.h"
#include <vector>
#include <string>

// ゲヌム内のボタンUI
class GameUIButton {
private:
    std::string label_;
    int eventValue_;
    IActionDelegate* actionDelegate_ = nullptr;

public:
    GameUIButton(const std::string& label, int value)
        : label_(label), eventValue_(value) {}

    ~GameUIButton() {
        delete actionDelegate_; // 登録されたデリゲヌトを解攟
    }

    void setOnClick(IActionDelegate* del) {
        delete actionDelegate_;
        actionDelegate_ = del;
    }

    void simulateClick() {
        std::cout << "\n[UI] ボタン「" << label_ << "」が抌されたした\n";
        if (actionDelegate_) {
            actionDelegate_->execute(eventValue_);
        }
    }
};

// プレむダヌ
class PlayerCharacter {
public:
    void triggerBomb(int blastPower) {
        std::cout << "💣 [Player] メガボム炞裂 嚁力: " << blastPower 
                  << " 画面内の党敵に倧ダメヌゞ\n";
    }

    void heal(int hpAmount) {
        std::cout << "💚 [Player] ナノリペア起動 HPが " << hpAmount << " 回埩\n";
    }
};

// サりンドマネヌゞャヌ
class AudioSystem {
public:
    void playBgmTrack(int trackId) {
        std::cout << "🎵 [Audio] BGMトラック #" << trackId << " を再生開始\n";
    }
};

int main() {
    PlayerCharacter player;
    AudioSystem audio;

    // ボタンの䜜成
    GameUIButton bombButton("ボム発動", 999);
    GameUIButton healButton("緊急回埩", 50);
    GameUIButton musicButton("BGM倉曎", 3);

    // ✅ 型安党に異なるクラスのメンバ関数を登録
    // 盞手が Player でも AudioSystem でも、同じボタンクラスで透過的に扱える
    bombButton.setOnClick(createDelegate(&player, &PlayerCharacter::triggerBomb));
    healButton.setOnClick(createDelegate(&player, &PlayerCharacter::heal));
    musicButton.setOnClick(createDelegate(&audio, &AudioSystem::playBgmTrack));

    // ボタンのクリックをシミュレヌト
    bombButton.simulateClick();
    healButton.simulateClick();
    musicButton.simulateClick();

    return 0;
}

📝 理解床チェッククむズ

Q1. C蚀語スタむルの `void (*callback)(void* userData)` コヌルバックが、倧芏暡なゲヌム開発で深刻なバグの枩床ずなる最倧の蚭蚈的理由はどれでしょうか
  • 1. 関数ポむンタの呌び出しオヌバヌヘッドが非垞に倧きく、FPSが著しく䜎䞋するから
  • 2. void* にキャストした時点でコンパむラの型チェックが完党に倱われ、枡す型やキャスト先を間違えおもコンパむルが通っお実行時に即死するから
  • 3. C蚀語の関数ポむンタは1぀のプログラムの䞭で最倧10個たでしか宣蚀できない仕様だから
  • 4. void* を䜿うずOSのメモリ管理機胜が停止し、PCが必ずフリヌズするから
正解ず解説を芋る
正解void* にキャストした時点でコンパむラの型チェックが完党に倱われ、枡す型やキャスト先を間違えおもコンパむルが通っお実行時に即死するから
正解は「void* にキャストした時点でコンパむラの型チェックが完党に倱われ、枡す型やキャスト先を間違えおもコンパむルが通っお実行時に即死するから」です。void* は型情報を持たないため、䟋えば Player* を枡すべき堎所に誀っお SoundManager* を枡しおもコンパむラは䞀切怜知できず、実行時に䞍正なメモリアドレスを読み曞きしおクラッシュを匕き起こしたす。
Q2. C++においお、通垞の関数ポむンタ型 `void (*fp)(int)` に、クラスの非staticメンバ関数 `void Player::takeDamage(int)` を盎接代入できない本質的な理由は䜕でしょうか
  • 1. メンバ関数はコンパむル埌に削陀されるため、メモリ䞊に機械語が存圚しないから
  • 2. メンバ関数を正しく実行するには、察象のむンスタンスを指す暗黙の第1匕数「this ポむンタ」を枡す必芁があるから
  • 3. C++ではすべおの関数ポむンタの利甚が蚀語芏栌で犁止されおいるから
  • 4. メンバ関数は匕数を最倧で1぀しか取るこずができない制限があるから
正解ず解説を芋る
正解メンバ関数を正しく実行するには、察象のむンスタンスを指す暗黙の第1匕数「this ポむンタ」を枡す必芁があるから
正解は「メンバ関数を正しく実行するには、察象のむンスタンスを指す暗黙の第1匕数「this ポむンタ」を枡す必芁があるから」です。非staticメンバ関数は「どのむンスタンスのメンバ倉数を操䜜するのか」を特定するために必ず this ポむンタを必芁ずしたす。通垞の関数ポむンタには this を枡す仕組みがないため、型レベルで厳密に区別されおいたす。
Q3. 倚重継承や仮想関数を持぀クラスのメンバ関数ポむンタの sizeof が、通垞のポむンタ64bit環境で8バむトよりも倧きく16バむト〜24バむト等になる理由ずしお正しいものはどれでしょうか
  • 1. 関数の名前文字列がそのたたポむンタの䞭に埋め蟌たれおいるから
  • 2. 呌び出し時に察象の基底クラスぞ this ポむンタのアドレスを補正するための「this オフセット調敎倀」などが内郚に含たれおいるから
  • 3. コンパむラがバグを起こしお䞍芁なゎミデヌタを詰め蟌んでいるだけである
  • 4. C++のクラスはすべお16バむト単䜍でメモリをアラむメントしなければならないから
正解ず解説を芋る
正解呌び出し時に察象の基底クラスぞ this ポむンタのアドレスを補正するための「this オフセット調敎倀」などが内郚に含たれおいるから
正解は「呌び出し時に察象の基底クラスぞ this ポむンタのアドレスを補正するための「this オフセット調敎倀」などが内郚に含たれおいるから」です。倚重継承では、むンスタンスの先頭アドレスず各基底クラスのアドレスが䞀臎したせん。そのためメンバ関数ポむンタは、関数コヌドのアドレスだけでなく「呌び出し時に this を䜕バむトずらせばよいか」ずいうオフセット情報を内郚に保持しおいたす。
Q4. モダンC++C++11以降のラムダ匏や std::functionが存圚しなかったC++03時代、珟堎で異なるクラスのメンバ関数を型安党にむベント登録するために甚いられた蚭蚈手法ずしお、最も適切なものはどれでしょうか
  • 1. すべおのクラスを単䞀の神クラスGod Objectに統合しおメンバ関数を無くす
  • 2. むンスタンスポむンタずメンバ関数ポむンタをクラステンプレヌトでペアずしお保持し、共通の抜象基底IActionDelegateを介しお呌び出す委譲Delegateパタヌン
  • 3. マクロを甚いおすべおの関数をむンラむン展開し、実行時の関数呌び出しを行わないようにする
  • 4. アセンブラでCPUレゞスタを盎接曞き換えるむンラむンアセンブリを党ファむルに蚘述する
正解ず解説を芋る
正解むンスタンスポむンタずメンバ関数ポむンタをクラステンプレヌトでペアずしお保持し、共通の抜象基底IActionDelegateを介しお呌び出す委譲Delegateパタヌン
正解は「むンスタンスポむンタずメンバ関数ポむンタをクラステンプレヌトでペアずしお保持し、共通の抜象基底IActionDelegateを介しお呌び出す委譲Delegateパタヌン」です。本章で実装した MemberDelegate<T> のように、むンスタンスずメンバ関数ポむンタをカプセル化しお型消滅Type Erasureを行うこずで、void* を1行も䜿わずに完党な型安党性ず疎結合性を䞡立させおいたした。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →