C/C++ / Python風のデコレータを書いてみたい ── Buffered Loop

前回からの続きです。

デコレータを「テンプレートを使って関数ポインタと関数オブジェクトを受け取り、値を返す関数」として定義しました。 また、新しい名前を付けるために移譲する関数や関数オブジェクトを作りました。

これを使って次のような要求を実装してみようと思います。

要求

  • streamから読みとった値を加工してstreamに流したい。
  • この時、全てを読み込んでから加工するとバッファから溢れてしまうので、 一定サイズ毎にステップで処理したい。
  • 加工方法は複数あるが、バッファの取り回しを共通化したい。

ここでは、std::iostreamに対して処理を行います。 std::iostreamならば、std::streambuf等、他にやりようがあるようです。 そちらを使った詳説は、詳しい方にお願いしたいと思います。 今回は、デコレータの例示ということでご勘弁ください。 実際には、iostreamではないCで書いた入出力インターフェースに対して実装してみた時の産物です。

ソースコード

ソースコードはhttps://bitbucket.org/hanepjiv/buffered_loopに上げました。 The MIT License (MIT)でお願いします。

buffered_loop.hpp

#ifndef HANEPJIV_BUFFERD_LOOP_HPP_
#define HANEPJIV_BUFFERD_LOOP_HPP_

#include <boost/type_traits.hpp>
#include <boost/array.hpp>

#include <exception>

namespace hanepjiv {

// ////////////////////////////////////////////////////////////////////////////
// ==================================================================
template <class BT = char*, const size_t BUFFER_SIZE = 1024 * 8>
struct BufferedLoop {
  template <class F>
  inline bool operator()(const F& a_func, size_t a_size) const {
    boost::array<typename boost::remove_pointer<BT>::type, BUFFER_SIZE> buffer;
    const size_t step = a_size / BUFFER_SIZE;
    for (size_t i = 0; i < step; ++i) {
      if (!((a_func)(BUFFER_SIZE, i, buffer.data(), BUFFER_SIZE))) {
        throw std::runtime_error("buffered_loop: Failed step.\n");
      }
    }
    const size_t rest = a_size % BUFFER_SIZE;
    if (rest != 0) {
      if (!((a_func)(BUFFER_SIZE, step, buffer.data(), rest))) {
        throw std::runtime_error("buffered_loop: Failed rest.\n");
      }
    }
    return true;
  }
};

}  // namespace hanepjiv


#endif  /* HANEPJIV_BUFFERD_LOOP_HPP_ */

BufferedLoopがデコレータです。 実際には、operator()がデコレータであり、 BufferedLoop自体はそのラップとしての役割を持つテンプレートクラスです。 テンプレート型の初期値を与えるためにクラス化してありますが、operator()が本質の部分です。

operator()内では、BUFFER_SIZEの大きさでバッファを確保し、a_size分の処理が終わるまで、バッファをa_funcに貸し出します。 バッファのサイズ、処理回数カウント、バッファの先頭ポインタ、処理すべきサイズを引数として渡しています。 a_sizeがBUFFER_SIZEで割りきれるとは限らないので、ループの後に残りのサイズ分を渡して、処理を抜けます。

BufferedLoopは処理に関して、何も関与していません。 バッファを確保し、細切れに使用してもらえるように複数回回しているだけです。

a_funcがfalseを返した場合、処理を切り上げて例外を投げています。

copy.hpp


#ifndef HANEPJIV_BUFFERED_LOOP_COPY_HPP_
#define HANEPJIV_BUFFERED_LOOP_COPY_HPP_

#include <boost/bind.hpp>

#include <iostream>

#include "./buffered_loop.hpp"

namespace hanepjiv {

// =================================================================
template <class BT> inline
bool copy_impl(std::ostream* a_dest, std::istream* a_src,
               const size_t a_buffer_size, size_t a_step,
               BT a_buffer, size_t a_size) {
  a_src->read(a_buffer, a_size);
  a_dest->write(a_buffer, a_size);
  *a_dest << std::endl;    // *** WARNING *** "erase this line!"
  return true;
}
// --------------------------------------------------------------------------------------------------------
template <const size_t BUFFER_SIZE>
inline bool copy(std::ostream* a_dest, std::istream* a_src, size_t a_size) {
  return BufferedLoop<char*, BUFFER_SIZE>()(
      boost::bind<bool>(&copy_impl<char*>,  a_dest, a_src,  _1, _2, _3, _4),
      a_size);
}

}  // namespace hanepjiv

#endif  /* HANEPJIV_BUFFERED_LOOP_COPY_HPP_ */

copy_implをデコレートして、copyを得ています。 BUFFER_SIZEをコンパイル時に指定して使用できるようになっています。

copy_implは、渡されたa_bufferに、a_srcからa_size分読み込み、そのままa_destに渡しています。 1step分の処理しか記述していません。バッファの確保やループ処理はデコレータが担当します。

デコレータでの呼び出しでは、a_srcやa_destについての記述がありませんでした。 呼び出しを可能にするために、boost::bindを使用しています。 これによって、クロージャという機能が使えるようになります。 pythonでは意識しなくても、クロージャが効いたのであえて説明していませんでした。 詳説は手に余るので、省かせていただきます。すみません。

乱暴にいえば、引数を渡すタイミングをずらすことができます。 copyの中では、a_dest,とa_srcだけを渡して、BufferedLoopの中で、残りの引数を渡してもらう仕組みになっています。

"*** WARNING ***" の部分は、テスト実行するときに出力を整形している処理です。実用用途では消す必要があります。

scramble.hpp

#ifndef HANEPJIV_BUFFERED_LOOP_SCRAMBLE_HPP_
#define HANEPJIV_BUFFERED_LOOP_SCRAMBLE_HPP_

#include <boost/bind.hpp>

#include <iostream>

#include "./buffered_loop.hpp"

namespace hanepjiv {

// =================================================================
template <class BT> inline
bool scramble_impl(std::ostream* a_dest, std::istream* a_src,
                   const size_t a_buffer_size, size_t a_step,
                   BT a_buffer, size_t a_size) {
  a_src->read(a_buffer, a_size);
  for (size_t i = 0; i < a_size; ++i) {
    a_buffer[i] += static_cast<char>(0x01u);  // SCRAMBLE!
  }
  a_dest->write(a_buffer, a_size);
  *a_dest << std::endl;    // *** WARNING *** "erase this line!"
  return true;
}
// ---------------------------------------------------------------------------------------------------------
template <const size_t BUFFER_SIZE>
inline bool scramble(std::ostream* a_dest, std::istream* a_src, size_t a_size) {
  return BufferedLoop<char*, BUFFER_SIZE>()(
      boost::bind<bool>(&scramble_impl<char*>,  a_dest, a_src,  _1, _2, _3, _4),
      a_size);
}

}  // namespace hanepjiv

#endif  /* HANEPJIV_BUFFERED_LOOP_SCRAMBLE_HPP_ */

copyとほぼ同じですが、a_destに書き込む前に、データに1を加えています。 これによって、渡したデータが加工されて出力されるということになります。

main.cc

#include <sstream>

#include "./copy.hpp"
#include "./scramble.hpp"

// ////////////////////////////////////////////////////////////////////////////
// ===================================================================
int main(int argc, const char* argv[]) {
  using namespace std;
  using namespace hanepjiv;

  // copy  ---------------------------------------------------------------------
  cout << "# COPY: input line." << endl << ">>> ";
  {
    string l_Str;
    getline(cin, l_Str);
    stringstream l_Input(l_Str);

    copy<2>(&std::cout, &l_Input, l_Str.length());
  }
  cout << endl << "# end COPY." << endl << endl;

  // scramble  -----------------------------------------------------------------
  cout << "# SCRAMBLE: input line." << endl << ">>> ";
  {
    string l_Str;
    getline(cin, l_Str);
    stringstream l_Input(l_Str);

    scramble<2>(&std::cout, &l_Input, l_Str.length());
  }
  cout << endl << "# end SCRAMBLE." << endl;

  return 0;
}

使用方法は上記の様になります。

標準入力から、一行分読み込み、標準出力にcopyやscrambleを適用しながら、出力します。

getlineやstringstreamは本質ではなく、入力を読み込むための処理です。

copyやscrambleの呼び出しが実質部分です。 今回はBufferの大きさとして2bytesを指定して、2bytes毎に処理します。 実際はありえないですね。1kから8kくらいでしょうか。完全に環境依存です。

コンパイルして実行してみます。 Makefileが入っているのでmakeが使用できます。


make

./buffered_loop 

# COPY: input line.
>>> 1234567890
12
34
56
78
90

# end COPY.

# SCRAMBLE: input line.
>>> 1234567890
23
45
67
89
:1

# end SCRAMBLE.

入力したデータが2bytes毎に出力されています。copyではそのままですが、scrambleでは1が足されて結果が加工されていることが解ります。

まとめ

Pythonのデコレータから始まり、話が派生してきました。 以上で、「C/C++ / Python風のデコレータ(decorator)を書いてみたい」というネタは終了です。

今更ですが、掲載したコードが実用になるかどうかは各自の判断にお任せします。 よろしくお願いします。

入力読み取りの部分をデコレータに吐き出して共通化することもできそうです。 うまくすれば多段化のように、繋げるかもしれません。 やってみる、とは言わないでおきます──。

目録

  1. C/C++ / Python風のデコレータを書いてみたい ── 関数ポインタ
  2. C/C++ / Python風のデコレータを書いてみたい ── 関数オブジェクト
  3. C/C++ / Python風のデコレータを書いてみたい ── テンプレート
  4. C/C++ / Python風のデコレータを書いてみたい ── Buffered Loop

0 件のコメント:

コメントを投稿