エラーハンドリング勉強会(リンク切れ: http://partake.in/events/9874b92a-4cf0-4a20-a3fe-951239da5612)にて、時間が余っているということで飛び入り発表していました。こんなことなら、初めから参加者として名乗りを上げておけば良かったです。

内容としては、C++11のexception_ptrの使い方を簡単に話しました。そのとき画面に映していたのはこれです: exception_ptr in エラーハンドリング勉強会

exception_ptrは「ライブラリを作るためのライブラリ」だと私は考えています。使わずに済むならそれに越したことはないです。とは言ったものの、ここではじかにexception_ptrを扱っていますけど。

以下は、そのとき発表した内容を基に、新しく書き起こしたものです。そのため、発表内容(「exception_ptr in エラーハンドリング勉強会」のスライド)とは厳密に一致していません。


さて、何か適当な処理を別のスレッドで行うという状況を考えましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 適当な関数
void f(int& result)
{
  // ここで時間のかかる処理
 
  result = 42;
}
 
int main()
{
  int result;
  std::thread t(f, std::ref(result));
 
  t.join();
  std::cout << result << std::endl;
}

最後に、呼出し元(のスレッド)で結果を受け取っていますね。いつもうまくいくのであればよいのですが、場合によっては例外を投げるのだとしたら、どうしましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyException {};
 
// 適当な関数
void f(int& result)
{
  // ここで時間のかかる処理
  int test = (rand() % 3);
  if (test == 0)
  {
    // 処理がうまくいかなければ例外を投げるかもしれない
    throw std::runtime_error("だめでした");
  }
  else if (test == 2)
  {
    throw MyException();
  }
 
  result = 42;
}

困ったことに、C++は例外で投げるオブジェクトの型に大きな制限がありません。極端な例では、intのような組込型でも可能です。Javaや.NETのように、特定の基底クラスから派生していること(かつ参照でオブジェクトを扱う)という条件の下であれば、単にExceptionやThrowable型のオブジェクトとして持ち運べばよいのですが、C++ではそうはいかないのです。

そこで、exception_ptrの出番です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void f(int& result, std::exception_ptr& ep)
{
  try
  {
    ep = nullptr;
 
    // ここで時間のかかる処理
    int test = (std::rand() % 3);
    if (test == 0)
    {
      // 処理がうまくいかなければ例外を投げるかもしれない
      throw std::runtime_error("だめでした");
    }
    else if (test == 2)
    {
      throw MyException();
    }
 
    result = 42;
  }
  catch(...)
  {
    ep = std::current_exception();
  }
}

このように、current_exception関数で例外オブジェクトをexception_ptrに捕まえます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{
  int result;
  std::exception_ptr ep;
  std::thread t(f, std::ref(result), std::ref(ep));
 
  t.join();
  try
  {
    if (ep != nullptr)
    {
      std::rethrow_exception(ep);
    }
    std::cout << result << std::endl;
  }
  catch(std::exception& e)
  {
    std::cerr << e.what() << std::endl;
  }
  catch(MyException const& e)
  {
    std::cerr << "だめだった" << std::endl;
  }
}

そして、rethrow_exceptionを使うとexception_ptrの中身をthrowできます。これをcatchすることで、中身を取り出せるというわけです。

最後に、これを小綺麗にラップしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 適当な関数
int f()
{
  // ここで時間のかかる処理
  int test = (std::rand() % 3);
  if (test == 0)
  {
    // 処理がうまくいかなければ例外を投げるかもしれない
    throw std::runtime_error("だめでした");
  }
  else if (test == 2)
  {
    throw MyException();
  }
  return 42;
}
 
int main()
{
  async<int> res(f);
 
  // 何かほかの処理
 
  try
  {
    std::cout << res.get_result() << std::endl;
  }
  catch(std::exception const& e)
  {
    std::cerr << e.what() << std::endl;
  }
  catch(MyException const& e)
  {
    std::cerr << "だめだった" << std::endl;
  }
}

こんな風になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
template<typename Result>
class async
{
public:
  template<typename F>
  async(F f) : t(invoker, std::ref(result), std::ref(ep), f) {}
  Result get_result()
  {
    t.join();
    if (result)
    {
      return *result;
    }
    else
    {
      std::rethrow_exception(ep);
    }
  }
private:
  std::thread t;
  boost::optional<Result> result;
  std::exception_ptr ep;
  static void invoker(
    boost::optional<Result>& result,
    std::exception_ptr& ep,
    std::function<Result ()> f)
  {
    try
    {
      result = f();
    }
    catch(...)
    {
      ep = std::current_exception();
    }
  }
  async(async const&) = delete;
  async& operator =(async const&) = delete;
};

もっとも、こういう「適当な関数を渡したら、新しくスレッドを作ってそこで実行してくれる」というようなものは、packaged_taskとunique_futureで可能のですので、実際にはそっちを使いましょう(参考: N2709 – 非同期実行のための Packaging Tasks – melpon日記 – C++すら(ry)。こいつらはC++11を待たずともBoostにもあります(Futures — Boost 1.47.0)。

なお、exception_ptr自体も、Boostに一応あります (exception_ptr — Boost 1.47)。ただし、とくにcurrent_exceptionがライブラリだけでは実現不可能なので、throwの代わりにboost::throw_exception関数を使わなければならないなどといった制限があります。std::exception_ptrが使える環境では、std::exception_ptrを使うことをおすすめします。

2016年11月23日追記:partake.inがサービスを終了し、ページが消滅したので、リンクを解除しました。


スポンサード リンク

この記事のカテゴリ

  • ⇒ exception_ptr in エラーハンドリング勉強会