C++ の小さな技術を紹介するシリーズ【小技C++ 全9回】#3<コールバック>

コールバックとは一般に、関数ポインタを媒介として関数の呼び出しを委譲する技法である。

入門

簡単な例を見てみよう。

#include <cstdio>

void aisatsu()
{
    printf( "Hello" );
}

この関数は標準出力に、"Hello" という文字列を印字する。

void invoke_callback( void (*callback)() )
{
    return callback();
}

この関数はコールバック関数を引数にとり、それを自身の中で呼び出す。C++の場合、引数の型は std::function で代用することもでき、以下のように記述する。

#include <functional>

void invoke_callback( const std::function<void()>& callback )
{
    return callback();
}

実際に、aisatsu 関数をコールバックとして、invoke_callback 関数の引数に渡してみよう。

int main()
{
    invoke_callback( aisatsu );
    printf( "World!" );

    return 0;
}
実行結果
HelloWorld!

invoke_callback 関数の引数に渡された関数ポインタに対して関数呼び出しが適用され、aisatsu 関数が実行されている。

応用

次の配列が持つ要素に、さまざまな操作を適用したいとしよう。

#include <array>

std::array<int, 4> values = { 0, 1, 2, 3 };

invoke_callback 関数を少し変形させて、コールバック関数に配列の要素を順に渡すよう定義する。

void invoke_callback( const std::function<void( int& )>& callback )
{
    std::array<int, 4> values = { 0, 1, 2, 3 };

    for( auto& v : values ) {
        // 要素を順に渡す
        callback( v );
    }
}

引数に渡すコールバック関数の実装を差し替えることで、適用したい操作を変更する。

  • すべての要素を標準出力に印字する
#include <cstdio>

void callback_print( int& v )
{
    printf( "v = %d\n", v );
}
callback_printの適用
int main()
{
    invoke_callback( callback_print );

    return 0;
}
実行結果
v = 0
v = 1
v = 2
v = 3
  • すべての要素を2倍にする
void callback_double( int& v )
{
    v *= 2;
}
callback_doubleの適用
int main()
{
    invoke_callback( callback_double );
    invoke_callback( callback_print );

    return 0;
}
実行結果
v = 0
v = 2
v = 4
v = 6

さらに応用

クラスのメンバ変数を操作するときに、コールバック関数を利用することで変更に強いコードを記述することができる。以下のようなクラスを例に見てみよう。

class Hero
{
    uint32_t m_life = 100; // 耐久値(ダメージを受けると減り、回復すると増える)
};

Hero クラスは m_life というprivate メンバ変数を持っている。この変数はダメージや回復を受けることで増減させたい。この値を操作するためにどのようなアクセサを用意すればよいだろうか。さまざまな方法が考えられるが、ここではコールバック関数を応用したアクセサの例を試してみよう。

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

class Hero
{
public:
    void     Take_Attack( const std::function<void( uint32_t& )>& );
    void     Take_Heal  ( const std::function<void( uint32_t& )>& );

private:
    uint32_t m_life = 100;
};

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

void Hero::Take_Attack( const std::function<void( uint32_t& )>& callback )
{
    callback( m_life );
    printf( "ダメージを受けた!現在の耐久力は %d です.\n", m_life );
}

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

void Hero::Take_Heal( const std::function<void( uint32_t& )>& callback )
{
    callback( m_life );
    printf( "回復した!現在の耐久力は %d です.\n", m_life );
}

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

次に、コールバック関数として、Hero クラスに渡すための操作関数を用意しておく。

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

void Attack_fromZako( uint32_t& hero_life )
{
    hero_life -= 10;
    printf( "ザコの攻撃!\n" );
}

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

void Attack_fromBoss( uint32_t& hero_life )
{
    hero_life -= 50;
    printf( "ボスの攻撃!\n" );
}

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

void Heal_byGoodLeaf( uint32_t& hero_life )
{
    hero_life += 10;
    printf( "いい薬草を使った!\n" );
}

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

void Heal_byGreatLeaf( uint32_t& hero_life )
{
    hero_life += 50;
    printf( "すごくいい薬草を使った!\n" );
}

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

コールバック関数を適用してみよう。

int main()
{
    Hero hero;

    hero.Take_Attack( Attack_fromZako );
    hero.Take_Heal  ( Heal_byGoodLeaf );
    hero.Take_Attack( Attack_fromBoss );
    hero.Take_Heal  ( Heal_byGreatLeaf );

    return 0;
}
実行結果
ザコの攻撃!
ダメージを受けた!現在の耐久力は 90 です.
いい薬草を使った!
回復した!現在の耐久力は 100 です.
ボスの攻撃!
ダメージを受けた!現在の耐久力は 50 です.
すごくいい薬草を使った!
回復した!現在の耐久力は 100 です.

攻撃を受けたり、回復をするときに共通して処理したいことは Take_AttackTake_Heal に記述しておき、数値の大きさはコールバック関数の実装に任せている。つまり、さらに強いボスや、効果の高い薬草を途中から追加したくなった場合でも、Heroクラスのアクセサに変更を加える必要がない。また、m_life の値が変更されるときは必ず Take_Attack もしくは Take_Heal を経由するため、想定外の書き換えが発生しにくいという利点もある。さらには、Attack_fromBossHeal_byGreatLeaf 関数は Hero クラスに直接アクセス(つまり、依存)していないことがわかるだろう。

ラムダ式との併用

この記事ではコールバック関数としてすべてグローバル関数を使った。C++では、std::function に対してラムダ式を代入することができるため、コールバック関数としてラムダ式を渡せる。this やオート変数をキャプチャすることで、より小回りの利くコールバックを容易に仕込むことが可能となった。

変更に強いコーディングを

Hero クラスの例でも紹介したように、コールバック関数を使ったアクセサの設計はオブジェクト間の依存関係を見直す1つの手段となり得る。settergetter を無暗に生産してしまう前に、メンバの公開範囲やその操作経路をいま一度見直そう。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.