boost.asioのタイムアウト処理を綺麗に書く方法教えて!

はじめに

boost.Asioでは、非同期処理のタイムアウト処理を当然行うことが出来るが
一般的なソケットのような、関数にタイムアウト時間を設定するような簡易な方法ではない

非同期処理とは別に、タイマーWaitを非同期で書き、非同期処理が終了すればタイマーをキャンセルし
タイマーが先に来れば 非同期処理をキャンセルしタイムアウト処理を行う
という 冗長な処理が必要である

それを、どのようにラップすればきれいに書けるのか?という話

まずはベタに書いてみる

beta
// タイムアウトを設定
deadline_timer.expires_from_now( boost::posix_time::milliseconds(timeout_ms));
deadline_timer.async_wait(
  [=](const boost::system::error_code &ec) {    // タイムアウトハンドラ
    if (ec == boost::asio::error::operation_aborted) {
      // タイマーキャンセル時 = Read成功時はここでは何も行わない
    }else{
      // タイムアウト発生
      socket.cancel();
      handle_timeout(ec); // タイムアウト処理する
    }
});

boost::asio::async_read(socket, response,
  boost::asio::transfer_at_least(1),
  [this, self](const boost::system::error_code& ec, std::size_t bytes_transferred) {  // async_readのハンドラ
    // タイムアウト前にRead出来たのでタイマーをキャンセルする
    deadline_timer.cancel();
    // Read成功時のハンドラ
    handle_read(ec);
});

お互いにキャンセルをし合う。キャンセルを忘れるとたちまちバグるので、何かラップしたい
冗長だし

タイマー処理をラップ試みる

タイマーWait処理をWrapする。タイムアウト時のコールバックを登録し、そこにSocketのキャンセルやタイムアウト時の処理
非同期命令はそのまま

deadline1
// ライブラリ
// タイムアウト時間、タイムアウト時のハンドラを登録する
void deadlineOperation(boost::asio::deadline_timer &timer,
  const unsigned int timeout_ms,
  std::function<void(const boost::system::error_code &)> handler_timeout) {

timer.expires_from_now(boost::posix_time::milliseconds(timeout_ms));
// タイマー設定
timer.async_wait(
  [=](const boost::system::error_code &ec) {
    if (ec != boost::asio::error::operation_aborted) {
      handler_timeout(ec);
    }
  });
}

// 実際に使う
deadlineOperation(deadline_timer_, timeout_ms_
  // タイムアウト時のハンドラ登録
  , [this, self](const boost::system::error_code &ec) {
    socket.cancel();
    handler_timeout();
  });

boost::asio::async_read(socket, response,
  boost::asio::transfer_at_least(1),
  [this, self](const boost::system::error_code& ec, std::size_t bytes_transferred) {  // async_readのハンドラ
    // タイムアウト前にRead出来たのでタイマーをキャンセルする
    deadline_timer.cancel();
    // Read成功時のハンドラ
    handle_read(ec);
});

非同期関数も引数に

非同期関数も引数にとるが、面倒になるだけでメリット少ない

deadline2
  // ライブラリ
  // タイムアウト時間、非同期関数、タイムアウト時のハンドラを登録する
  void deadlineOperation2(boost::asio::deadline_timer &timer,
    const unsigned int timeout_ms,
    std::function<void()> handler,
    std::function<void(const boost::system::error_code &)> handler_timeout) {

  timer.expires_from_now( boost::posix_time::milliseconds(timeout_ms));
  timer.async_wait(
    [=](const boost::system::error_code &ec) {
      if (ec != boost::asio::error::operation_aborted) {
        handler_timeout(ec);   // タイムアウトしたときのハンドラ
      }
    });

  handler();
}


deadlineOperation2(deadline_timer_, timeout_ms_
  // 非同期関数登録
  , [this,self](){
    boost::asio::async_read(socket_, response_,
      boost::asio::transfer_at_least(1),
      [this, self](const boost::system::error_code& ec, std::size_t bytes_transferred) {
        deadline_timer_.cancel();
    handle_read());
      }
    );
  }
  // タイムアウト時のハンドラ登録
  , [this, self](const boost::system::error_code &ec) {
    socket.cancel();
    handler_timeout(ec);
  });

タイマー、ソケットのキャンセル関数、タイムアウトハンドラ関数をクラス内に持つ

実際に使うときは、タイムアウト値やタイムアウト処理は共通にする事多いのでコンテキストのメンバに持たせてみる

deadline3
  // ライブラリ
template<class T>
void deadlineOperation3(T &t,
  const unsigned int timeout_ms) {

  t.deadline_timer_.expires_from_now( boost::posix_time::milliseconds(timeout_ms));
  t.deadline_timer_.async_wait(
    [=](const boost::system::error_code &ec) {
      if (ec != boost::asio::error::operation_aborted) {
        t.handle_timeout(ec);    // タイムアウトしたときのハンドラ
      }
    });
}


deadlineOperation3<ThisClass>(this, timeout_ms_);

boost::asio::async_read(socket_, response_,
    boost::asio::transfer_at_least(1),
    [this, self](const boost::system::error_code& ec, std::size_t bytes_transferred) {
      deadline_timer_.cancel();
      handle_read_(ec);
    });
);

んー どうなんだろう?

タイムアウトクラスをshared_ptrにし destructorでタイマーキャンセルする

タイムアウトクラスをshared_ptrで作成
非同期関数でshared_ptrをキャプチャする
非同期関数抜けた時にキャプチャが外れ タイムアウトクラスのdestructor発動
自動的にタイマーキャンセルされる

deadline3
// ライブラリ
class deadlineOperation3 /*: public std::enable_shared_from_this<deadlineOperation3>*/{
private:
  boost::asio::deadline_timer deadline_timer_;
public:
  deadlineOperation3(boost::asio::io_service &io_service, unsigned int timeout_ms
    , std::function<void(const boost::system::error_code &)> handle_timeout)
    : deadline_timer_(io_service) {

  deadline_timer_.expires_from_now( boost::posix_time::milliseconds(timeout_ms));

  deadline_timer_.async_wait(
    [=](const boost::system::error_code &ec) {
      if (ec != boost::asio::error::operation_aborted) {
    handle_timeout(ec);
      }
    });
  };

  virtual ~deadlineOperation3() {
    // デストラクタでタイマーキャンセルする
    deadline_timer_.cancel();
  };
};


// 使う側
  // shared_ptrを使い タイムアウトクラス生成。タイムアウト時のコールバック登録
  auto timer = std::make_shared<asioUtil::deadlineOperation3>(io_service_, 1000,
    [this,self](const boost::system::error_code &ec) {
      socket_.cancel();
      handle_timeout();
    });

  boost::asio::async_read(socket_, response_,
    boost::asio::transfer_at_least(1),
    // timerをキャプチャする。コールバックを抜けるとdestructerにより タイマーがキャンセルされる仕組み
    [this, self, timer](const boost::system::error_code& ec, std::size_t bytes_transferred) {
      handle_read(ec);
  });

タイマーのキャンセル処理を隠すことが出来たが 少しやりすぎか?

最後に

もっといい方法ないかな?
非同期関数をWrapして タイマーのキャンセルやらも行いたかったけど暗黒コードになりそうだし
自分にそんな力ないので断念