この記事では、C++におけるコールバック処理の概要と、基本的な使い方について紹介します。
C++の環境構築がまだの方は、以下の記事をご覧ください。
コールバックとは
C++におけるコールバックは、関数ポインタや関数オブジェクトを使って、関数の実行を他の関数に委譲する手法です。「関数の実行を他の関数に委譲する」と言うと少し分かりにくいですが、ある関数(呼び出し元)が他の関数(コールバック)に対して、その関数(コールバック)を実行する役割を任せるということです。
コールバックを使うメリット
- 柔軟な処理の実行
-
コールバックを使うことで、特定のタイミングやイベントが発生したときに実行する処理を動的に変更できます。これにより、プログラムの動作を柔軟に制御することができます。
- コードの再利用性の向上
-
コールバックを使うことで、汎用的な関数を作成し、その関数内で行う具体的な処理を外部から渡すことができます。これにより、同じコードを異なる文脈で再利用しやすくなります。
- モジュール性の向上
-
コールバックを利用することで、異なるモジュール間の依存関係を減らし、コードを分離しやすくなります。これにより、コードのメンテナンスやテストが容易になります。
- イベント駆動型プログラミングの実現
-
コールバックはイベント駆動型プログラミングの基本的な構成要素です。例えば、GUIプログラムやサーバーサイドプログラムでは、ユーザーの操作や特定のイベントに応じて処理を実行する必要があります。コールバックを使うことで、これらのイベントハンドリングを実現できます。
- 非同期処理のサポート
-
非同期処理の完了を待つ間に他の処理を実行するためにコールバックが使用されます。非同期処理が完了したときにコールバック関数を呼び出すことで、効率的な並行処理が可能になります。
コールバックは、イベント駆動型のプログラミングや非同期処理などでよく使われます。
サンプルコード
コールバックの代表的な使い方を、以下の4つのパターンで紹介します。
関数ポインタを使ったコールバック
関数ポインタを使うのが最も基本的な方法です。
#include <iostream>
// コールバック関数の型を定義
typedef void (*CallbackFunction)();
void registerCallback(CallbackFunction callback) {
callback();
}
// コールバックとして登録される関数
void myCallback() {
std::cout << "Callback called!" << std::endl;
}
int main() {
// コールバックを登録
registerCallback(myCallback);
return 0;
}
typedef void (*CallbackFunction)();
CallbackFunction
という名前で関数ポインタ型を定義しています。この型は、引数がなく、戻り値がvoid
である関数へのポインタを指します。
void registerCallback(CallbackFunction callback) {
callback();
}
registerCallback
関数は、CallbackFunction
型の引数callback
を受け取ります。この関数は、受け取ったコールバック関数を呼び出します。
void myCallback() {
std::cout << "Callback called!" << std::endl;
}
myCallback
関数は、コールバック関数として使用される関数です。この関数は、標準出力に「Callback called!」と出力します。
int main() {
// コールバックを登録
registerCallback(myCallback);
return 0;
}
registerCallback(myCallback)
によって、myCallback
関数をコールバックとしてregisterCallback
関数に渡しています。registerCallback
関数内で、渡されたmyCallback
関数が呼び出され、結果として「Callback called!」というメッセージが標準出力に表示されます。
関数オブジェクト(ファンクター)を使ったコールバック
関数オブジェクト(ファンクター)もコールバックとして利用できます。
#include <iostream>
class MyCallback {
public:
void operator()() {
std::cout << "Callback called!" << std::endl;
}
};
template <typename Callback>
void registerCallback(Callback callback) {
callback();
}
int main() {
MyCallback myCallback;
// コールバックを登録
registerCallback(myCallback);
return 0;
}
class MyCallback {
public:
void operator()() {
std::cout << "Callback called!" << std::endl;
}
};
MyCallback
クラスは、関数オブジェクト(ファンクター)として機能します。operator()
メンバ関数を定義することで、MyCallback
クラスのインスタンスを関数のように呼び出すことができます。
template <typename Callback>
void registerCallback(Callback callback) {
callback();
}
registerCallback
はテンプレート関数です。任意の型Callback
を受け取ります。引数として渡されたcallback
を実行します。
int main() {
MyCallback myCallback;
// コールバックを登録
registerCallback(myCallback);
return 0;
}
main関数では、MyCallback
クラスのインスタンスmyCallback
を作成し、registerCallback
関数にmyCallback
を渡します。
registerCallback
関数内で、渡されたmyCallback
オブジェクトが関数のように呼び出され、結果として「Callback called!」というメッセージが標準出力に表示されます。
少し複雑なのでポイントを以下にまとめます。
- 関数オブジェクト(ファンクター)
-
MyCallback
クラスは、operator()
を定義することで関数オブジェクトとして機能します。これにより、インスタンスを関数のように呼び出すことができます。 - テンプレート関数
-
registerCallback
関数はテンプレート関数として定義されており、任意の型を受け取ることができます。これにより、関数オブジェクトや関数ポインタなど、さまざまな型のコールバックを受け取ることができます。
テンプレート関数が何かわからない方は以下の記事をご覧ください。
std::functionとラムダ式を使ったコールバック
C++11以降では、std::function
とラムダ式を使うと柔軟で強力なコールバック機能が得られます。
#include <iostream>
#include <functional>
// std::functionを使ってコールバックの型を定義
typedef std::function<void()> CallbackFunction;
void registerCallback(CallbackFunction callback) {
callback();
}
int main() {
// ラムダ式をコールバックとして登録
registerCallback([]() {
std::cout << "Callback called!" << std::endl;
});
// 通常の関数をコールバックとして登録
auto myCallback = []() {
std::cout << "Another callback called!" << std::endl;
};
registerCallback(myCallback);
return 0;
}
typedef std::function<void()> CallbackFunction;
std::function
を使って、引数を取らず戻り値がvoid
である関数型をCallbackFunction
という型名で定義します。std::function
は、様々な種類の関数や関数オブジェクトをラップするための汎用的なクラステンプレートです。
void registerCallback(CallbackFunction callback) {
callback();
}
registerCallback
関数は、CallbackFunction
型の引数callback
を受け取り、受け取ったコールバック関数を実行するために、callback()
を呼び出します。
// ラムダ式をコールバックとして登録
registerCallback([]() {
std::cout << "Callback called!" << std::endl;
});
registerCallback
に対して、無名のラムダ式を直接渡しています。このラムダ式は引数を取らず、単に「Callback called!」というメッセージを標準出力に出力します。
// 通常の関数をコールバックとして登録
auto myCallback = []() {
std::cout << "Another callback called!" << std::endl;
};
registerCallback(myCallback);
auto myCallback = []() { ... };
を使って、myCallback
という名前のラムダ式を定義し、それをregisterCallback
に渡しています。このラムダ式は「Another callback called!」というメッセージを標準出力に出力します。
このコードのポイントは以下です。
- 柔軟性
-
std::function
を使うことで、ラムダ式や通常の関数を同じように扱えます。これにより、コールバックの実装がより柔軟になります。 - 型安全性
-
std::function
は型安全なラッパーであり、コーディングのミスがあればコンパイル時に型の不一致エラーが検出できます。 - コンパクトさと可読性の向上
-
ラムダ式はその場で関数を定義し、それを直接使用することができます。これにより、コードがよりコンパクトになります。コールバックの定義と使用が同じ場所に集約されるため、コードの理解やメンテナンスが容易になります。
メンバ関数をコールバックとして使用
最後はstd::bind
を使ってクラスのメンバ関数をコールバックとして使う方法です。
#include <iostream>
#include <functional>
class MyClass {
public:
void myMemberCallback() {
std::cout << "Member callback called!" << std::endl;
}
};
typedef std::function<void()> CallbackFunction;
void registerCallback(CallbackFunction callback) {
callback();
}
int main() {
MyClass obj;
// メンバ関数をコールバックとして登録
registerCallback(std::bind(&MyClass::myMemberCallback, &obj));
return 0;
}
class MyClass {
public:
void myMemberCallback() {
std::cout << "Member callback called!" << std::endl;
}
};
MyClass
クラスに、myMemberCallback
というメンバ関数を持たせています。
int main() {
MyClass obj;
// メンバ関数をコールバックとして登録
registerCallback(std::bind(&MyClass::myMemberCallback, &obj));
return 0;
}
&MyClass::myMemberCallback
は、MyClass
クラスのmyMemberCallback
メンバ関数へのポインタを表します。
std::bind
を使って、&MyClass::myMemberCallback
を&obj
にバインドしています。これにより、obj
のインスタンスでmyMemberCallback
が呼び出されるようにしています。
このコードのポイントは以下です。
- メンバ関数をコールバックに使える
-
std::bind
を使うことで、メンバ関数をコールバックとして登録できます。通常の関数ポインタやラムダ式ではクラスのインスタンスを指定する方法が直接的ではないため、このような場面で便利です。