🐣

C++: コールバックのデザインパターン

2023/11/12に公開

C++ (C++14) でコールバックをメソッドの引数に渡す方法についていくつかのデザインパターンをまとめています。間違いやその他の方法等あればお気軽にコメント頂けると幸いです。

基礎: メソッドを引数で渡す方法

初心者の方向けに、はじめにメソッドを引数で渡す方法について解説します。

やり方としては、関数ポインタを直接指定する方法と、もう少し分かり易くするためにstd::functionを利用する2種類の方法があります。C++の場合は、std::functionを使う方が良いと思います。

1) 関数ポインタを渡すやり方

サンプルプログラムを以下に示します。CalcクラスのsetMehodメソッドの引数にadd関数を渡すサンプルです。C言語同様のベーシックな方法です。

#include <iostream>
#include <memory>

class Calc {
 public:
  Calc() = default;
  ~Calc() = default;

  void setMethod(int (*method)(int a, int b)) {
    method_ = method;
  }

  int invokeMethod(int a, int b) {
    if (method_)
      return method_(a, b);
    return -1;
  }

 private:
  int (*method_)(int a, int b);
};

int add(int a, int b) {
  return a + b;
}

int main() {
  auto calc = std::make_unique<Calc>();

  calc->setMethod(add);
  std::cout << calc->invokeMethod(1, 2) << std::endl;

  return 0;
}

2) std::functionを利用するやり方

ラムダ式とセットになる事が多いstd::functionを利用して、もう少し見易くします。現在の主流はこちらだと思います。上記の関数ポインタを利用したサンプルプログラムからの変化点だけ以下に示します。

こちらの方が直感的に分かり易いですよね?

using CalcMethod = std::function<int(int a, int b)>; // ← 追加

class Calc {
 public:
  Calc() = default;
  ~Calc() = default;

  void setMethod(CalcMethod method) { // ← 変更
    method_ = method;
  }

  int invokeMethod(int a, int b) {
    if (method_)
      return method_(a, b);
    return -1;
  }

 private:
  CalcMethod method_; // ← 変更
};

実践的なコールバック (リスナー/ハンドラ) の登録方法

実践的なプログラムでは、何かのイベント発生時に登録しておいたコールバックをイベントの状況に合わせて呼び出す事が多くあります。その場合のデザインパターンをいくつか解説します。

1) 必要なだけ引数でコールバックを登録

以下のサンプルプログラムのように、Messenger.setHandlerメソッドでイベントハンドラを登録するやり方です。

#include <iostream>
#include <memory>

using MessengerOnSuccess = std::function<void(void)>;
using MessengerOnError = std::function<void(const std::string& message)>;

class Messenger {
 public:
  Messenger() = default;
  ~Messenger() = default;

  void setHandler(MessengerOnSuccess on_success, MessengerOnError on_error) {
    on_success_ = on_success;
    on_error_ = on_error;
  }

  void sendMessage(const std::string& message) {
    if (message.size() == 0) {
      if (on_error_)
        on_error_("error message");
      return ;
    }

    // メッセージ送信処理など

    if (on_success_)
      on_success_();
  }

 private:
  MessengerOnSuccess on_success_;
  MessengerOnError on_error_;
};

int main() {
  auto messenger = std::make_unique<Messenger>();

  messenger->setHandler(
    []() {
      std::cout << "call OnCuccess()" << std::endl;
    },
    [](const std::string& message) {
      std::cout << "call OnError(): " << message << std::endl;
    }
  );

  messenger->sendMessage("Send message");
  messenger->sendMessage("");

  return 0;
}

2) Interface Classを利用

上記の1)の方法でもコールバックを登録する事が可能ですが、引数が多くなったり、保守性が低かったりするため、複数のコールバックを登録する場合にはこれから解説する方法を利用する事が多いと思います。

C++にはJavaのinterfaceのような機能がないため、自前でInterface Classを作成し、それを継承したクラスをハンドラ (リスナー) としてコールバック登録します。

1)のサンプルプログラムと同様の内容をInterface Classを利用して書き換えたものを2パターン示します。

2-1) Interface Classを継承したクラスに処理を記述するパターン

MessegeHandlerがInterface Classで、これを継承したMessegeHandlerImpleに、コールバックが呼ばれたときの処理を記述する実装です。

#include <iostream>
#include <memory>

class MessegeHandler {
 public:
  MessegeHandler() = default;
  virtual ~MessegeHandler() = default;

  void onSuccess() {
    onSuccessInternal();
  }

  void onError(const std::string& message) {
    onErrorInternal(message);
  }

 protected:
  virtual void onSuccessInternal() = 0;
  virtual void onErrorInternal(const std::string& message) = 0;
};

class MessegeHandlerImple : public MessegeHandler {
 public:
  void onSuccessInternal() override {
      std::cout << "call OnCuccess()" << std::endl;
  }

  void onErrorInternal(const std::string& message) override {
      std::cout << "call OnError(): " << message << std::endl;
  }
};

class Messenger {
 public:
  Messenger() = default;
  ~Messenger() = default;

  void setHandler(std::unique_ptr<MessegeHandler> handler) {
    handler_ = std::move(handler);
  }

  void sendMessage(const std::string& message) {
    if (message.size() == 0) {
      if (handler_)
        handler_->onError("error message");
      return ;
    }

    // メッセージ送信処理など

    if (handler_)
      handler_->onSuccess();
  }

 private:
  std::unique_ptr<MessegeHandler> handler_;
};

int main() {
  auto messenger = std::make_unique<Messenger>();
  auto handler = std::make_unique<MessegeHandlerImple>();

  messenger->setHandler(std::move(handler));

  messenger->sendMessage("Send message");
  messenger->sendMessage("");

  return 0;
}

2-2) 汎用的な実装

2-2)のように、MessegeHandlerImpleの中に必要な実装を書いてしまうと、多用するようなAPI (ライブラリ) で複数のMessegeHandlerImpleのようなクラスを複数作成する必要が出てきます。また、onSuccessメソッドやonErrorメソッドのうち、必要なメソッドだけ指定して上位から利用するということも出来なくなります。

その解決策として、MessegeHandlerImpleのインスタンス変数でコールバックを渡せるような実装にするパターンを以下に示します。

#include <iostream>
#include <memory>

class MessegeHandler {
 public:
  MessegeHandler() = default;
  virtual ~MessegeHandler() = default;

  void onSuccess() {
    onSuccessInternal();
  }

  void onError(const std::string& message) {
    onErrorInternal(message);
  }

 protected:
  virtual void onSuccessInternal() = 0;
  virtual void onErrorInternal(const std::string& message) = 0;
};

using MessegeHandlerImpleOnSuccess = std::function<void(void)>;
using MessegeHandlerImpleError = std::function<void(const std::string& message)>;

class MessegeHandlerImple : public MessegeHandler {
 public:
  MessegeHandlerImple(MessegeHandlerImpleOnSuccess on_success,
                      MessegeHandlerImpleError on_error)
      : on_success_(on_success), on_error_(on_error) {}
  virtual ~MessegeHandlerImple() = default;

 protected:
  void onSuccessInternal() override {
    if (on_success_)
      on_success_();
  }

  void onErrorInternal(const std::string& message) override {
    if (on_error_)
      on_error_(message);
  }

 private:
  MessegeHandlerImpleOnSuccess on_success_;
  MessegeHandlerImpleError on_error_;
};

class Messenger {
 public:
  Messenger() = default;
  ~Messenger() = default;

  void setHandler(std::unique_ptr<MessegeHandler> handler) {
    handler_ = std::move(handler);
  }

  void sendMessage(const std::string& message) {
    if (message.size() == 0) {
      if (handler_)
        handler_->onError("error message");
      return ;
    }

    // メッセージ送信処理など

    if (handler_)
      handler_->onSuccess();
  }

 private:
  std::unique_ptr<MessegeHandler> handler_;
};

int main() {
  auto messenger = std::make_unique<Messenger>();
  auto handler = std::make_unique<MessegeHandlerImple>(
    []() {
      std::cout << "call OnCuccess()" << std::endl;
    },
    [](const std::string& message) {
      std::cout << "call OnError(): " << message << std::endl;
    }
  );

  messenger->setHandler(std::move(handler));

  messenger->sendMessage("Send message");
  messenger->sendMessage("");

  return 0;
}

3) Javaっぽく作る方法 (C++っぽくないので非推奨)

Javaっぽく書く方法を示しておきます。ただし、これはC++っぽくはないので、推奨する方法ではありません。

MessegeHandlerをclassではなく、構造体として定義する方法です。

#include <iostream>
#include <memory>

using MessegeHandlerOnSuccess = std::function<void(void)>;
using MessegeHandlerError = std::function<void(const std::string& message)>;

struct MessegeHandler {
 public:
  MessegeHandler(MessegeHandlerOnSuccess on_success,
                 MessegeHandlerError on_error)
      : onSuccess(on_success), onError(on_error) {}

  MessegeHandlerOnSuccess onSuccess;
  MessegeHandlerError onError;
};

class Messenger {
 public:
  Messenger() = default;
  ~Messenger() = default;

  void setHandler(std::unique_ptr<MessegeHandler> handler) {
    handler_ = std::move(handler);
  }

  void sendMessage(const std::string& message) {
    if (message.size() == 0) {
      if (handler_)
        handler_->onError("error message");
      return ;
    }

    // メッセージ送信処理など

    if (handler_)
      handler_->onSuccess();
  }

 private:
  std::unique_ptr<MessegeHandler> handler_;
};

int main() {
  auto messenger = std::make_unique<Messenger>();
  auto handler = std::make_unique<MessegeHandler>(
    []() {
      std::cout << "call OnCuccess()" << std::endl;
    },
    [](const std::string& message) {
      std::cout << "call OnError(): " << message << std::endl;
    }
  );

  messenger->setHandler(std::move(handler));

  messenger->sendMessage("Send message");
  messenger->sendMessage("");

  return 0;
}

Discussion