言ってしまえば、高階関数は便利だねという話でもあるわけです、それに加えてC++特有の事情があるだけで。


C++視点からのRuby紹介 // Speaker Deckの52ページ目の「ブロックを使ったRAIIっぽい機能」より。

ブロックを使ったRAIIっぽい機能

File.open('some_file') do |file|
  # 処理
end
  • File.openメソッド
    • ファイルを開いた後、受け取ったブロックを実行
    • 例外の発生有無にかかわらず最後にファイルを閉じる
  • スコープを抜けると自答的にリソースが閉じられる
    • RAIIと共通している

このような書き方はC++でも有用です。なぜなら、C++には事実上「デストラクタで例外を投げてはいけない」ためです(cf. More Effective C++ 項目11 デストラクタで発生した例外を抑える)。

そこで、ブロック引数のように関数(オブジェクト)を実引数に取る関数という方法です。後処理に失敗する可能性のある処理を抽象化する方法として有力な方法の1つです。

例として、RubyのFile.openに相当する関数をC++で作ってみます。

#include <iostream>
#include <fstream>
#include <string>
#include <exception>
#include <type_traits>
#include <functional>
//#include <boost/range/algorithm/copy.hpp>
 
template<typename Char>
inline void on_failure(std::basic_fstream<Char>& fs) {
  fs.exceptions(std::ios_base::iostate());
  fs.close();
  if (!fs) {
    std::throw_with_nested(std::ios_base::failure("close"));
  } else {
    throw;
  }
}
 
template<typename F, typename Char = char>
auto file_open(const char* filename, std::ios_base::openmode mode, F&& f)
  -> typename std::enable_if<
    !std::is_same<decltype(f(std::declval<std::fstream&>())), void>::value,
    decltype(f(std::declval<std::fstream&>()))>::type
{
  std::basic_fstream<Char> fs(filename, mode);
  fs.exceptions(std::ios_base::failbit | std::ios_base::badbit);
  try {
    auto result = f(fs);
    fs.close();
    return result;
  } catch (...) {
    on_failure(fs);
  }
}
 
template<typename F, typename Char = char>
auto file_open(const char* filename, std::ios_base::openmode mode, F&& f)
  -> typename std::enable_if<
    std::is_same<decltype(f(std::declval<std::fstream&>())),
    void>::value, decltype(f(std::declval<std::fstream&>()))>::type
{
  std::basic_fstream<Char> fs(filename, mode);
  try {
    fs.exceptions(std::ios_base::failbit | std::ios_base::badbit);
    f(fs);
  } catch (...) {
    on_failure(fs);
  }
}
 
int main() {
  auto s = file_open("src.txt", std::ios_base::in, [](std::istream& is) {
    return std::string(
      std::istreambuf_iterator<char>(is),
      std::istreambuf_iterator<char>());
  });
  std::cout << s << std::endl;
  file_open("dst.txt", std::ios_base::out, [&](std::ostream& os) {
    std::copy(s.cbegin(), s.cend(), std::ostreambuf_iterator<char>(os));
    //boost::copy(s, std::ostreambuf_iterator<char>(os));
  });
}

このように、closeでの失敗をちゃんと拾って呼び出し元へthrowする処理が書けました。

まとめです。

  • 一般的に後処理で失敗の可能性がある処理は、デストラクタに任すわけにはいかない。
  • その制約を超える手段として、関数オブジェクトを引数に取る関数(Rubyならブロック引数を取るメソッド)を作るという手段がある。

ファイルIO(特に失敗の検出・エラー処理)については、気が向いたら別の記事として書くかもしれません。


ファイルは閉じるまでがIOです(「家に帰るまでが遠足です」調で)。

Man page of CLOSE

close() の返り値のチェックはよく省略されるが、 これは深刻なプログラミングエラーである。 前の write(2) 処理に関するエラーが最後の close() のときになって初めて通知される場合がありうる。 ファイルクローズの際に返り値をチェックしないと、 気付かないうちにデータを失ってしまうかもしれない。 これは特に NFS やディスククォータを使用した場合に見られる。

スポンサード リンク

この記事のカテゴリ

  • ⇒ RAII以外にもある後処理の書き方
  • ⇒ RAII以外にもある後処理の書き方