可変長テンプレートを使った C++11 時代の可変長引数関数

C++ では printf など可変長の引数をとる関数を結構使うことはあります。 ただ、 C 言語由来である従来の可変長引数は多くの問題を抱えています。
そんな中、 C++ は C++11 仕様で大きな機能追加が行われました。 その一つである可変長テンプレート(Variadic Templates)によって、 可変長引数の扱いは大きく改善されました。
今回は C++11 を経た、新時代の可変長引数関数の書き方について解説したいと思います。

従来方式の問題点

最初に従来方式の問題点の確認です。

サンプルコード : oldargs.cpp

整数値の合計

整数値の合計を求める関数を従来方式で書くと、次のようになります。
  int unsafe_isum(int argnum, ...)
{
  va_list arg;
  int sum = 0;
  
  va_start(arg, argnum);

  // 引数の数だけ繰り返し
  for (int cnt = 0; cnt < argnum ; cnt++)
  {
    // int として処理
    sum += va_arg(arg, int);
  }

  va_end(arg);
  
  return sum;
}
  // 使用例
  printf("sum = %d\n", unsafe_isum(3, 1, 2, 3));  // 9
"..." がなんでも受け取れる引数の指定で、 va_start(), va_end() で囲んだ中に処理を書き込みます。 この辺はオマジナイのようなものです。

一つ目の問題点は引数の数がわからないところです。
わからないので、最初の引数で数を指定して、引数の数だけ処理しています。 もし間違って数が少なければ全部処理しませんし、多ければ不定の動作となります。

二つ目の問題点は型が不明という点で、最も問題です。 va_arg() のところで整数が渡されたものとしています。 これはキャストではないため、もし 1.0 のような値を渡すと double を int として処理して変な値となります。

printf

もう一つ例として printf の実装をみてみます。
void unsafe_printf(const char *fmt, ...)
{
  va_list arg;

  va_start(arg, fmt);
  ::vprintf(fmt, arg);
  va_end(arg);
}
  unsafe_printf("Hello %s, %02d, %.3f\n", "world", 3, 1.2);  // "Hello world, 03, 1.200"
vprintf()というのは printf のような関数を自作するために用意されている関数で va_list を引数として渡します。
ここがもう一つ問題で、可変長引数を別の可変長引数の関数に渡せません。 va_list を渡す関数が用意されていればいいですが、 ライブラリーなどで可変長引数の関数だけ公開されていると それをラップするような可変長引数の関数を作ることができません。

もちろん、他の 2 つの問題も顕在です。
printf の場合、引数の数はフォーマットの文字列の % の数で判断します。
型は %s, %d などの指定で決めます。 文字列のところにうっかり整数を渡したりすると変なポインターアドレスとなってコアダンプします。 そういった経験をしたことがある人も多いのではないでしょうか。

問題点のまとめ

まとめると引数の主な問題点は 3 つです。
  1. 数が分からない
  2. 型がわからない
  3. 可変長引数のまま関数に渡すことができない
また 2 番目はややこしい問題を引き起こしたりもします。
例えば、 ポインターを受け取る引数に NULL ポインターのつもりで 0 を渡したとします。 たいてい整数は 32 ビットなのに対し、ポインター型は 64 ビットなので、 32 ビット分ゴミが入ります。 これがうまくいったり、いかなかったりする不具合になります。

わかりづらかったでしょうか。でも心配はいりません。 今後はこんな問題からオサラバできます。

対応と他の言語との比較

C++11 では、可変長テンプレートの関数テンプレートを使って、可変長引数の従来の問題に対応できるようになっています。
ちなみにここでは紹介しませんが、可変長テンプレートにはクラステンプレートもあり、 こちらは tuple 型などを作るときに役立ちます。

この章では実際の書き方に入る前に、他の言語での対応と比較してみていきます。

動的型付け言語、 C++ 以外の静的型付け言語

他の言語ではどうやっているかというと "引数の可変長の部分を配列(リスト)として受け取って、ループで処理する" といったことが多いです。


Ruby を始めとする動的型付けの言語ではオブジェクト自体が型情報を持っています。 そのため、 配列にはすべてのオブジェクトが格納可能です。
静的型付けでも C# のような言語では、すべてのクラスの基底クラス(Object)があるので、 Object の配列として受け取ることができます。 ただ、 Object として受け取るのは、問題がないわけではありません。
この残りの引数を配列に入れるやり方は動的型付けに向いたやり方です。 任意の型を配列に入れるといった利便性こそ動的型付けの魅力とも言えます。(その分、安全性、速度が犠牲になることもありますが) C++ は静的型付けです。向いていないどころか、共通基底クラスもないため、配列で受け取ってループで回すということが自体ができません。 そこで、新しい C++ ではどういった解決方法をとったかというと ループではなく、再帰を使います。

関数型言語

可変長テンプレートによる書き方は、関数型言語を知っている人であれば、 「パターンマッチと似てるな」と感じると思います。
関数型には変数を変更できないという縛りがあるため、 こちらもループが回せないという共通点があります。


また、再帰というとスタックがたまるので、 メモリーを心配される人もいるかと思います。
しかし、テンプレートなので、コンパイル時にはすべて展開されます。 現状、各コンパイラーの対応がどうなっているかはよく知りませんが、 これはコンパイラーが頑張れば解決できる問題だと思います。

ちなみに関数型言語では、末尾再帰最適化といって、 再帰をループとして最適化する機能が言語自体に備わっていることが多いです。

可変長テンプレート関数

前置きが終わったので、これから可変長テンプレート関数の使い方について説明していきます。


サンプルコード : sum.cpp

整数の合計

サンプルは整数値を合計する関数にしました。
// 末端 isum
inline int isum()
{
  return 0;
}

// 再帰呼び出し isum
template<typename First, typename... Rest>
int isum(const First& first, const Rest&... rest)
{
  return first + isum(rest...);
}
  cout << isum(1, 2, 3, 4) << endl;         // 10
テンプレート引数 Rest や 関数の引数 rest のについている ...pack 演算子 といいます。 これが可変長引数の残りを意味していて、 rest の変数にそれがまとめられています。
サンプルで一回目に呼ばれた状況では rest = 2, 3, 4 です。

pack された変数に対してできる操作は一つだけです。 それが rest のにつけている ... で unpack 演算子 と呼び、 まとめた変数を展開(unpack)して元に戻します。
一回目では rest... => 2, 3, 4 と展開され、これを isum(first, ...rest) 関数に渡しています。
  return 1 + isum(2, 3, 4);
これが再帰的に繰り返されます。
  isum(1, 2, 3, 4)        // first = 1,  rest = { 2, 3, 4 }
  1 + isum(2, 3, 4)       // first = 2,  rest = { 3, 4 }
  1 + 2 + isum(3, 4)      // first = 3,  rest = { 4 }
  1 + 2 + 3 + isum(4)     // first = 4,  rest = {}
  1 + 2 + 3 + 4 + isum()  // 末端関数の呼び出し
"..." として 0 から任意の個数の引数が受け取れます。 そのため、ここが空になるまで、繰り返し用の isum が呼び出されます。
その後、次の流れになります。
  1. 空になった rest を展開(rest...) → isum()
  2. 引数なしの isum() の呼び出し
    • 先頭の引数がないため、もう再帰呼び出しの isum に合致しない
  3. 繰り返しが終了
    • isum() の中では再帰呼び出しをしていない


この可変長テンプレート引数を使った方法では従来の問題点が解消されています。
  1. 引数の数を指定する必要がない
  2. double の値を渡してもエラーではなく、 C++ の変換ルール(少数点以下切り捨て)で変換
double の変換を細かくいうと、テンプレート引数 First が double となり、戻り値のところで整数として変換されます。
  cout << isum(1.0, 2.2, 3.3, 4.9) << endl; // 10
                                            // = 1.0 + 2 + 3 + 4 + isum()

任意の型の合計

参考として任意の型の合計用の関数もあげておきます。
template<typename Ret>
Ret any_sum()
{
  return Ret();
}
 
template<typename Ret, typename First, typename... Rest>
Ret any_sum(const First& first, const Rest&... rest)
{
  return first + any_sum<Ret>(rest...);
}
使う場合、戻り値の型(Ret)は引数から判断できないので、指定する必要があります。
  cout << any_sum<int>(1.0, 2.2, 3.3, 4.9) << endl;    // 10
  cout << any_sum<double>(1.0, 2, 3.3, 4.9) << endl;   // 11.2
  cout << any_sum<string>("Hello", " ", "world", "!") << endl;  // "Hello world!"
  // cout << any_sum<string>("Hello ", "world", 3) << endl;  // コンパイルエラー
テンプレートはダックタイピングなので + 演算子とデフォルトコンストラクターさえあれば string 型などにも使えます。 ただし、 string と整数の + など解決できない演算がある場合にはコンパイルエラーです。

型安全な printf

この可変長テンプレートを使えば、型に対して安全な printf が作成できます。
可変長テンプレートを使った printf は Wikipedia でも紹介されています。 ただ、 printf を 0 から作ると大変なので、 今回は boost::format を使って作ってみます。

Boost Format

Boost Format は危険な printf の解決策として作られたもので、 % 演算子のオーバーロードを使っています。


format.cpp :
  boost::format fmt("Hello %s, %02d, %.3f");
  fmt = fmt % "world" % 3 % 1.2;
  std::cout << fmt << std::endl;  // Hello world, 03, 1.200
より詳しい使い方については以下の記事を見られると良いと思います。

簡易 printf

boost::format を使って printf を作ると次のようになります。

safe_printf.cpp :
inline void safe_printf(boost::format &bfmt)
{
  std::cout << bfmt;
}
 
template<typename First, typename... Rest>
void safe_printf(boost::format &bfmt, const First& first, const Rest&... rest)
{
  bfmt = bfmt % first;
  safe_printf(bfmt, rest...);
}
   boost::format fmt("Hello %s, %02d, %.3f\n");
   safe_printf(fmt, "world", 3, 1.2);  // Hello world, 03, 1.200
   safe_printf("Hello %s, %02d, %.3f\n", "world", 3, 1.2);  // Hello world, 03, 1.200
可変長部分ではない第一引数がある以外は合計の例と同じです。

様々な printf 系関数への展開

従来方式の問題点 2 つが解決されていることについては isum のところで説明しました。 残り 1 つの問題点は可変長引数のまま他の可変長引数に渡せないことでした。
これは、すでに少し出ていますが、 unpack 演算子を使って解決できます。

printf には sprintf, fprintf のような類似の関数がありますが、 この可変長引数関数から可変長引数関数の呼び出しを利用して作ることができます。

safe_printfs.cpp


まず、ベースとなる関数を作ります。
inline void safe_format(boost::format &fmt)
{  
}


template<typename First, typename... Rest>
void safe_format(boost::format &fmt, const First& first, const Rest&... rest)
{
  fmt = fmt % first;
  safe_format(fmt, rest...);
}
これを使って printf 系の関数を実装します。
// 安全版 sprintf
template<typename... Rest>
std::string safe_sprintf(const char *fmt, const Rest&... rest)
{
  boost::format bfmt(fmt);
  safe_format(bfmt, rest...);
  return boost::str(bfmt);
}


// 安全版 fprintf
template<typename... Rest>
void safe_fprintf(std::ostream &out, const char *fmt, const Rest&... rest)
{
  boost::format bfmt(fmt);
  safe_format(bfmt, rest...);
  out << bfmt;
}


// 安全版 printf
template<typename... Rest>
void safe_printf(const char *fmt, const Rest&... rest)
{
  safe_fprintf(std::cout, fmt, rest...);
}
  const char *fmt = "Hello %s, %02d, %.3f\n";
  
  std::cout << safe_sprintf(fmt, "world", 3, 1.2);
  safe_fprintf(std::cout, fmt, "world", 3, 1.2);
  safe_printf(fmt, "world", 3, 1.2);

文字列の結合

途中紹介した文字列の合計では整数を渡すとエラーでした。 このままだと気持ち悪いので、 最後に「任意の型を渡して文字列に変換して結合する関数」の例も紹介します。
(サンプルは sum.cpp 内に記述しています)

inline void concat_internal(std::stringstream &sout)
{
}
 
template<typename First, typename... Rest>
void concat_internal(std::stringstream &sout, const First& first, const Rest&... rest)
{
  sout << first;
  concat_internal(sout, rest...);
}


template<typename... Args>
std::string concat(const Args&... args)
{
  std::stringstream sout;
  concat_internal(sout, args...);
  return sout.str();
}
  cout << concat("Hello ", "world ", 3, " ", 1.2) << endl;  // "Hello world 3 1.2"
stringstream を使って文字列に変換しています。 実は boost::format の内部でも stream を利用しています。
これらは ostream の出力を実装していれば、自作のクラスでも使うことができます。

参考

スポンサーサイト



 

C++ のコピーコンストラクターと代入演算子

C++ でクラスを作ったり、メンバーを追加した場合、必ずやらなければならないことがあります。 それは 「コピーコンストラクター代入演算子 が必要か適切に判断する」ということです。 また、必要だったとして、これらを書く場合にも様々な注意点やテクニックがあります。 今回はコピーコンストラクターと代入演算子に関する話題についてまとめてみたいと思います。

なお、今回は Effective C++ の影響をかなり受けています。 Effective C++ と同じような内容を書くと問題あるかと思い、 それらに関してはサワリと本の項の参照先だけ書いています。
(項は第 2 版のものです)

コピーコンストラクター、代入演算子 とは

まず、この 2 つについて、概要を説明します。

コピーコンストラクター同じクラスのオブジェクトを引数にとるコンストラクター を指します。
  Foo a;
  Foo b(a);  // コピーコンスタクター
代入演算子同じクラスのオブジェクトを代入する時 に使われます。
  Foo a, c;
  c = a;     // 代入演算子
この 2 つは同じようなことをやるので、基本的にセットで考えます。 最初(初期化時)にコピーするのがコピーコンストラクターで、途中でコピーするのが代入演算子です。
ちょっと意外かもしれませんが、初期化時には代入に見えてもコピーコンストラクターが呼ばれます。
  Foo a;
  Foo d = a; // コピーコンスタクター
コピーコンスタクター、代入演算子の 2 つには大きなポイントがあります。
  • 定義しなくても使える(デフォルトのものが呼び出される)
  • デフォルトのものだと問題が生じることがある

定義方針

前章のポイントを踏まえてると、コピーコンストラクターと代入演算子を定義する方針は次のようになります。
copy_policy.png


なぜこういう方針になるのか、定義時の注意事項も含め、順に解説していきます。

デフォルトの仕様

コピーコンストラクター、代入演算子は記述しなくても、コンパイラーが自動で作成してくれるため、 コピー、代入を行うことができます。

このことを知らないのか、知っててもちゃんと判断できないのか、 やたら書く人がいます。
しかし、これはよくありません。 時間の無駄ですし、メンバーの追加や継承を行った際に忘れてしまう危険性が出てきます。 不必要なコピーコンストラクター、代入演算子は定義しないようにしなければなりません。


不必要かどうか判断するためには、デフォルトで作成されるコピーコンストラクター、代入演算子 がどういうものか知っておく必要があります。

デフォルトで作成されるものでも、memcpy のような単純なコピーではありません。 「各メンバーに対して、コピーコンストラクターあるいは代入演算子を順に呼び出す」 という仕様になっています。
  • Effective C++ (45 項) C++ がどんな関数を黙って書き、呼び出しているか知っておこう
例えば、次のようなクラスがあったとします。

noncopy_sample.cpp :
class Person
{
private:
    std::string m_name;
    int m_age;
    // :
};
int など基本型のメンバーに関してはデフォルトのもので十分なのは、すぐわかると思います。
string のクラス自体は内部に動的に割り当てるメモリーを持っています。 しかし、string がコピーコンストラクター、代入演算子を持っていて、 それを呼び出すため、問題なく処理されます。
よって、このクラスではコピーコンストラクター、代入演算子の定義は不要です。

コピーコンストラクター、代入演算子の定義が必要な場合

定義が必要なのはメモリーを動的に割り当てるクラスです。 これはポインターのコピーでは同じものを指すために生じる問題です。

cpp_copy_alloc.png
  • Effective C++ (11 項) メモリを動的に割り当てるクラスでは、コピーコンストラクタと代入演算子を宣言しよう

コピーコンストラクター、代入演算子の定義方法

コピーコンストラクターと代入演算子の定義の書式は次のようになります。
C(const C &);
C &operator=(const C &);
具体的なサンプルをあげます。

copy_sample.cpp : (抜粋)
class Person
{
  char *m_name;                         ///< 名前
  unsigned int m_age;                   ///< 年齢

public:
  /// コンストラクター
  Person(const char *name = 0, unsigned int age = 0)
      :m_name(0), m_age(age)
  {
    SetName(name);
  }

  /// デストラクター.
  virtual ~Person()
  {
    SetName(0);
  }


  /// コピーコンストラクター
  Person(const Person &other)
      :m_age(other.m_age)
  {
    SetName(other.m_name);
  }

  Person &operator=(const Person &other)
  {
    // 自身の代入チェック
    if (this != &other) {      
      SetName(other.m_name);
      m_age = other.m_age;
    }
    return *this;
  }

  // :  

};
コピーコンストラクターと代入演算子を書く上でいくつか注意点があります。

代入演算子の戻り値は自身の参照

代入演算子では *this を返してます。
  Person &operator=(const Person &other)
  {
      // :
    return *this;
  }
ここは void でも実装は可能です。 ただ、そうすると基本型のように a = b = 1; といった連続した代入ができなくなリます。 そのため、自身の参照(*this)を返すのが流儀です。
  • Effective C++ (15 項) operator = を書くときは、*this へのリファレンスを返そう
また、代入演算子に限らず、セッターでは参照を返すというメソッド連鎖(Method chaining)という考え方もあります。

代入演算子では自身の代入チェック

サンプルで this かどうかをチェックしている部分です。
  Person &operator=(const Person &other)
  {
    // 自身の代入チェック
    if (this != &other) {      
        // :
    }
    return *this;
  }
コピーコンストラクターとは違い、代入では自身が渡される可能性があります。 代入演算子を定義した場合には必ず自身のチェックを入れる習慣をつけた方がよいです。
  • Effective C++ (17 項) operator = では、自分自身へ代入するケースをチェックしよう

継承クラスでのコピー、代入

コピーコンストラクター、代入演算子で間違えやすいのが、 継承したクラスの場合です。

copy_sample.cpp : (抜粋)
class Student : public Person
{
  char *m_school;                         ///< 学校名

public:
  /// コンストラクター
  Student(const char *name = 0, unsigned int age = 0, const char *school = 0)
      :Person(name, age)
  {
    SetSchool(school);
  }

  /// デストラクター.
  virtual ~Student()
  {
    SetSchool(0);
  }

  /// コピーコンストラクター
  Student(const Student &other)
      :Person(other)
  {
    SetSchool(other.m_school);
  }

  Student &operator=(const Student &other)
  {
    // 自身の代入チェック
    if (this != &other) {
      // 基底クラスメンバーの代入
      Person::operator=(other);
      SetSchool(other.m_school);
    }
    return *this;
  }

  // :
  
};
コピーコンストラクターでは初期化子で基底クラスのコピーコンストラクターを指定する必要があります。 これを忘れると基底クラスのメンバーはデフォルトコンストラクターで初期化されてしまいます。
  Student(const Student &other)
      :Person(other)  // 基底クラスのコピーコンスタクター
  {
代入演算子でも明示的に基底クラスの代入演算子を呼び出す必要があります。
  Student &operator=(const Student &other)
  {
      // :
      // 基底クラスメンバーの代入
      Person::operator=(other);
      SetSchool(other.m_school);
  • Effective C++ (16 項) operator = では、すべてのデータメンバに代入しよう

代入演算子によるコピーコンストラクター実装の是非

DRY(Don't Repeat Yourself)原則とあるように、同じことを繰り返さないのがいいプログラミングです。
コピーコンストラクター、代入演算子は同じような処理を行います。 コピーコンストラクターを代入演算子を使って実装することを考えてみます。
  Person(const Person &other)
  {
    *this = other;
  }
個人的にはこれも 「悪くはない」と思います。
ただ、これだとデフォルトでの初期化の後、代入となります。 そのため、メンバーの初期化演算子で設定した方が、若干速くなるはずです。 コピーコンストラクターも初期化子を使って別途、実装した方がよりよいです。

また、メンバーが const やリファレンスとなっている場合、 それらはそもそもコンストラクターでしか設定できません。 そういった場合は代入の方は後述の「禁止」にすることが多いと思います。
  • Effective C++ (12 項) コンストラクタでは、代入よりも初期化を使おう

コピー、代入の禁止

通常のメンバー関数は、定義しなければ使えません。 しかし、コピーコンストラクター、代入演算子は書かなくても自動で作成されます。 そのため、使えないようするには明示的に「禁止」する必要があります。

コピー、代入の禁止が必要なケース

前章のコピーコンストラクター、代入演算子の定義が必要な場合だったとしても、 アプリケーションのクラスではコピー、代入の処理自体が必要ない場合も結構あります。 こういった場合はわざわざ実装せず、コピー、代入の禁止にします。


代入できないメンバーを持つ場合も代入を禁止します。代入できないメンバーは次の 2 つです。
  • const
  • リファレンス
ただし、コピーコンストラクターによるコピーは可能です。コピーも禁止するかどうかは場合によりますが、 たいていは一緒に禁止します。


コピー、代入してはいけないメンバーを持つ場合もあります。
例えば、fstream やファイルポインター(FILE *)のようなファイルを扱う変数です。 これらはコピーして複数で使うとおかしなことになります。
なお fstream のように次節以降の方法でコピー、代入が禁止され、使えないようになっていることもあります。

禁止方法 - 基本

コンスタクターが自動で作ってしまう関数を禁止するには 関数を private にします。
  • Effective C++ (27 項) 暗黙のうちに生成される不要なメンバ関数は、明示的に使用を禁止しよう
コピー、代入を禁止する基本的な方法はコピーコンストラクター、代入演算子を private 領域に書くことです。
class Person
{  
 private:
  // コピー禁止
  Person(const Person &);
  void operator=(const Person &);
ここで代入演算子の戻り値を void にしていますが、 「戻り値だけ違う関数は定義できない」ため、なんでもかまいません。

禁止方法 - マクロ

コピー、代入を禁止するクラスを作ることは結構あります。 毎回書くのが面倒だったり、禁止する目的を明確にしたりするため、 次のようなマクロを使うこともあります。
/// コピー禁止マクロ
/// @param C クラス名
#define NON_COPYABLE(C)       C(const C &);       \
                              void operator=(const C &)
これを使う場合、 このマクロを private 領域に記述します。
class Person
{
 private:
  // コピー禁止(マクロ版)
  NON_COPYABLE(Person);
ただ、C++ ではマクロはあまり推奨されていませんし、 マクロ自体をコーディング規約で禁止されていることもあります。
  • Effective C++ (27 項) #define ではなく、const と  inline を使おう
禁止用のマクロに関しては inline 関数にして代用するということもできません。

禁止方法 - 継承 (boost::noncopyable)

直接書かなくても、private な継承を利用して関数を private にすることも可能です。 そのためのクラスを用意しておけば、マクロのように記述を簡略化、明確化することができます。
Boost では、すでにそのクラスが用意されており、すぐに使うことができます。
#include <boost/noncopyable.hpp>

class Person : boost::noncopyable
{
ただし、boost::noncopyable は使えない場合があります。
それは 他のクラスを継承しなければならない場合です。 その場合、多重継承で noncopyable を継承することになりますが、 boost::noncopyable は多重継承に対応していません。

多重継承に対応しようとするともう少し複雑になります。 ここまでやるなら、もう直接書くかマクロでもいいかなとは思います。

禁止方法 - C++11

C++11 では、関数の使用を禁止する delete 指定 の機能が追加されました。 これを使って、コピーコンストラクター、代入演算子を禁止すると次のようになります。
class Person
{
  public:
  // コピー禁止 (C++11)
   Person(const Person &) = delete;
   Person &operator=(const Person &) = delete;
マクロにすると以下のような感じです。
/// コピー禁止マクロ (C++11)
/// @param C クラス名
#define NON_COPYABLE(C)         C& operator=(const C&) = delete;        \
                                C(const C&) = delete;
従来の方法と違い、 private 領域に書かないといけないという縛りがなくなります
また、コンパイル時のエラーメッセージも「private なメソッドを使っている」から「削除されたメソッドを使っている」という旨のメッセージに変わり、原因が分かりやすくなっています。

なお、実際に C++11 を使う場合は移動コンストラクター、移動代入演算子もも考慮した方がいいのです。 そちらについては以下の記事をご覧ください。

サンプルコード

コピー禁止のサンプルコードです。



 

C++14 Streams を使った関数型のデータ処理

C# の LINQ、Java の Stream のように「関数型のデータ処理」が多くの言語で取り入れられてきています。 そんな中、C++11,14 での機能追加によって、 C++ でもとうとう Steams ライブラリーを使えば、それができるようになりました。
そこで今回はこの関数型のデータ処理の魅力と Streams の使い方について紹介したいと思います。

関数型データ処理の魅力

関数型データ処理の何が素晴らしいのかを端的にいえば、従来よりもコードを短くかつわかりやすく記述することができることです。

範囲 for の魅力

C++11 では 範囲に基づく for 文 という機能も追加されました。 関数型データ処理の良さの説明のために、まず「範囲 for 」の良さを見てみます。

例えば、 STL コンテナーの各要素に対して、何か処理をする場合、 vector であればループカウンターを回すと思います。 より汎用性を高めるにはイテレーターを使うでしょう。
  vector<int> ary = { 3,2,9,6 };

  for (auto itr = ary.begin() ; itr != ary.end() ; itr++) {
    cout << *itr << endl;
  }  
これを 範囲 for を使って書くと次のようになります。
  vector<int> ary = { 3,2,9,6 };

  for (auto elem : ary) {
    cout << elem << endl;
  }  
比べてみると 何故この機能が追加されたのか分かりやすいのではないでしょうか?
カウンターやイテレーターをいじる操作が無くなるので、短く書けています。 それでいて、余計な処理がないので、コンテナーの要素に対する何か処理しているという意図が分かり易くなっています。

関数型データ処理

関数型データ処理の魅力は 範囲 for に通じるものです。 範囲 for では逐次処理しかできませんが、それを多くの処理に展開したものと言えます。

範囲 for は見方を変えると 要素を引数とした処理を行うコードの固まり(コードブロック)をコンテナーに適用しています。 関数型データ処理ではこのコードブロックの記述に関数内に関数定義が書けるラムダ式を使っています。
  vector<int> ary = { 3,2,9,6 };

  MakeStream::from(ary) |
      for_each([](auto elem) {
          cout << elem << endl;
        });
なお、先ほどの for やラムダ式の引数を (auto elem) にしていますが、 これは要素が整数だからです。サイズの大きな要素の場合はもちろん (const auto &elem) にしておく必要があります。

また、詳しくは後述しますが、 処理を連結できることも分かりやすさを向上しています。 その際、遅延評価によって、効果的な連結になるようになっています。

言語への広がり

関数型のデータ処理というのは特に呼び名がなかったので、 関数型プログラミングでよく使われていることから、私がてきとうに付けた名前です。
関数型のパラダイムでは変数を変更することができません(参照透過性)。 そのため、ループカウンターなどは使えず、こういった高階関数を使うスタイルになります。


関数型というと 「関数型言語は C++ や Java のようなオブジェクト指向言語よりも短く書ける」 と以前は言われていました。
しかし、そのコードが短くなる理由のほとんどが関数型のデータ処理にあります。 このデータ処理スタイルは「関数型でなければ使えない」 といったものではありません。 実際、今では多くの言語で取り入れられてきており、そのメリットを享受することができます。 なかでも、Ruby は言語レベルでコードブロックの機能を持ち、高階関数を使うよりもエレガントに書けるようになっています。

C++14 Streams

関数型のデータ処理を C++ で使えるようにするためのライブラリーが Streamsです。 その実現には C++11, 14 の機能を使っており、 C++14 以上に対応していないとコンパイルは通りません。

ただ、 Java の Streams から来ているのでしょうが、 C++ では stream というのはすでにあるので、名前が紛らわしいです。 処理のイメージとしては stream であっているのですが、「もうちょっと名前はなんとかならなかったのかな」 とは思います。


Streams はヘッダーをインクルードすれば、使えるタイプのライブラリーです。
ヘッダーはホームページの [Download Streams] のリンクからからダウンロードできます。

cpp_streams_dl.png


圧縮ファイルを展開した source ディレクトリーにヘッダーファイルがあります。 インクルードパスに追加し、 "Streams.h" をインクルードすると使えるようになっています。
#include "Stream.h"
Streams ライブラリーのクラスはすべて stream の名前空間で定義されています。
また、 +, - などの演算子のオーバーロードが定義されていますが、 そちらは stream::op の名前空間です。
使用する場合 stream:: をつけるか、 using を使うかです。演算子を使う場合は using を使います。
以降のサンプルでは using を使ったとして、名前空間は記述していません。
using namespace stream;
using namespace stream::op;

Streams の対象となるデータ

Streams の対象となるデータは 配列 と std::vector のような STL コンテナー などです。
ただ、そのままでは適用できないので、 MakeStream::from() の関数で Stream のオブジェクトにします。
Stream<T> MakeStream::from(const Container& cont);
Stream<T> MakeStream::from(T* arr, size_t length);
Stream<T> MakeStream::from(std::initializer_list<T> init);
  vector<int> vec = { 3,2,9,6 };
  int cary[] = { 3,2,9,6 };
  MakeStream::from(vec);
  MakeStream::from(cary, sizeof(cary)/sizeof(cary[0]));  // 配列はサイズも必要
  MakeStream::from({ 3,2,9,6 });                         // 初期化リストでも OK  
Container は正確には STL のコンテナーである必要はなく、 begin, end のメソッドが定義されているクラスであれば何でもかまいません。開始、終了のイテレーターを渡してオブジェクトを作ることもできます。
Stream<T> MakeStream::from(Iterator begin, Iterator end);
他言語ではコンテナー以外のいろいろなものを同じように処理できるのがメリットの一つなのですが、 C++ ではそこまでではありません。しかし、イテレーターにすることによって、多少いろいろなものが処理できるようにはなっています。

input_iterator.cpp : (入力ストリームのイテレーターのサンプル)
  auto seq = MakeStream::from(istream_iterator<int>(cin), istream_iterator<int>());
  cout << endl;
  for (const auto &elem : seq) {
    cout << elem << endl;
  }
$ ./a.exe 
3 2 9 6 <Ctrl+D>
3
2
9
6
また、 from() 以外にも 0,1,2, ... といった無限順列を生成する counter() など多くの生成関数(Genelator)が用意されています。 ただ、テストやサンプル以外で使うことはあまりないです。

基本的な関数

Stream にはたくさんのメソッドが使えます。 しかし、多すぎて関数型のデータ処理になれていない人にとっては 「どれを使えばいいの」ということになると思います。
そこで、まず抑えておくべき重要な 3 つのメソッドについて紹介します。
  1. 写像(map_)
  2. フィルター(filter)
  3. 畳み込み、縮退(reduce)
各関数は " | " 演算子でStream オブジェクトを受け取って適用する形になります。

map_ : 写像

写像というのは map, mapping とも呼ばれる処理で、 配列などのコレクションのメンバーに一つずつ関数を適用して 戻り値で新しいコレクションを作ります。

cs_linq_map.png

Streams では、map はすでに std::map があるので、 map_ という関数名になっています。
MakeStream::from({3, 2, 9, 6})
    | map_([](auto elem) { return elem * 2; });  // 6 4 18 12 
map_ に渡す関数は、各要素を引数にとる関数で、その戻り値が新しいコレクションの要素となります。 渡す関数の戻り値の型を変えれば、 型を変えた新しいコレクションを作ることもでき、用途の広いメソッドです。

filter : フィルター

フィルターはコレクションの中から条件にあう要素を取り出す処理です。

cs_linq_filter.png

MakeStream::from({3, 2, 9, 6})
    | filter([](auto elem) { return elem % 2 == 1; });  // 3 9

reduce : 縮退(畳み込み)

要素を順に取得し、それらを計算に使った結果を返す処理です。
cs_linq_fold.png
この処理は指定した関数の結果を重ねていくため 畳み込み(fold)と呼ばれます。
また、要素が順に減っていって一つの値になるため、縮退(reduce)とも呼ばれます。 Streams では縮退の方の reduce という関数名です。
std::vector<int> src = {3, 2, 9, 6};
MakeStream::from(src) | reduce([](auto sum, auto elem) { return sum + elem; }); // 20
MakeStream::from(src) | reduce([](auto max, auto elem) { return (max < elem) ? elem : max; }); // 9
reduce に渡す関数は 2 つの引数を取り、 2 つ目が各要素で、 関数の戻り値が次に呼ばれた時の 1 つ目の引数です。
合計の例を順に記述すると次のようになります。
{3, 2, 9, 6}

(3, 2) => 3 + 2 ↓
                (5, 9) => 5 + 9  ↓
                                (14, 6) => 14 + 6  ↓
                                                   20
  
渡した関数は 2 つの目の要素から呼ばれ、最初の引数 1 つ目の要素になります。 identity_reduce() を使って、初期値を渡して、最初の要素から処理することもできます。
MakeStream::from(src) | identity_reduce(0, [](auto count, auto elem) { return count + 1; })); // 4
map, filter がコレクションから新しいコレクションを返す関数の基本なのに対し、 reduce はコレクションの各要素から値(スカラー値)を算出 する関数の中でもっとも基本的な関数です。
sum(), max() や count() の関数は予めライブラリーに用意されていますが、 サンプルコードのように reduce() で記述ことが出来ます。

その他の関数

データ処理の定番の関数以外で、抑えておいた方がいいかなと思うものも挙げて置きます。
limit
指定した範囲の要素を取得
MakeStream::counter(1) | limit(3);  // {1 2 3}
    
any, all
要素どれか一つ(すべて)が条件を満たすかどうかの判定。
MakeStream::from({3, 2, 9, 6}) | any([](auto elem) { return (elem % 3) == 0;});  // true
MakeStream::from({3, 2, 9, 6}) | all([](auto elem) { return (elem % 3) == 0;});  // false
    
sort
要素のソート
MakeStream::from({3, 2, 9, 6}) | sort();  // {2 3 6 9}      
    
演算子
Stream オブジェクトに対する四則演算など。
MakeStream::from({3, 2, 9, 6}) / 3.0; // {1.0 0.666667 3.0 2.0}
5 * MakeStream::from({3, 2, 9, 6});   // {15 10 45 30}
MakeStream::from({3, 2, 9}) + MakeStream::from({1, 2, 3}); // {4 4 12}

    
print_to
Stream オブジェクトのダンプ
MakeStream::from({3, 2, 9, 6}) | print_to(std::cout);  // 3 2 9 6      
    

メソッドの連結と遅延評価

メソッドの連結

Stream の関数は " | " 演算子の後に記述しますが、 これを連結して書けるようになっています。
Unix 系のコマンドラインをご存じの人にはパイプみたいと思われるはずです。 パイプのイメージを持ってもらって間違いないです。
MakeStream::from({3, 2, 9, 6})
      | filter([](auto elem) { return elem % 2 == 1; })  // {3 9 }
      | map_([](auto elem)  { return elem * 2; })        // {6 18 }
      | reduce([](auto sum, auto elem) { return sum + elem; });  // 6+18 => 24

連結の終わり

連結できるのは map_() や filter() 関数の戻り値が Stream のオブジェクトだからです。
最後に forEach で逐次処理したり、 reduce で値の算出に使うと連結が終わります。 終わらずに得た変数は範囲 for で使うこともできます。
しかし、 STL コンテナーなどに戻したいといった場合には to_container を呼ぶ必要があります。 container には vector や list を記述し、それぞれのコンテナーへ変換します。
MakeStream::from({3, 2, 9, 6})
      | filter([](auto elem) { return elem % 2 == 1; })  // {3 9 }
      | map_([](auto elem)  { return elem * 2; })        // {6 18 }
      | to_vector();                                     // [6 18 ]

遅延評価

関数を連結して書くと、要素を溜めているため、 ループで書くよりもメモリーがもったいないと思われる人もいるかもしれません。
しかし、必要になるまで実行(評価)されないという遅延評価(Lazy evaluation) の機能によって、 無駄がないようになっています。

前節の連結の処理は一見、次のように処理してるように見えます。
              filter           map
{3, 2, 9, 6}    →   {3, 9}    →   {6, 18}
しかし、実際には遅延評価により、メソッドごとに結果をためるのではなく、 1 要素ずつ流すように次のメソッドに渡していきます。
   filter        map
3    →    3     →    6
2    →    ☓
9    →    9     →    18
6    →    ☓
実際に上記の順で処理されているかは peek() で確認できます。 peek() は渡した関数を実行し、要素をそのまま次に渡す確認用のメソッドです。
  auto lazyseq =
      MakeStream::from({3, 2, 9, 6})
      | peek([](auto elem) { std::cout << "[in]  : " << elem << std::endl; })
      | filter([](auto elem) { return elem % 2 == 1; })
      | peek([](auto elem) { std::cout << " - filter : " << elem << std::endl; })
      | map_([](auto elem)  { return elem * 2; })
      | peek([](auto elem) { std::cout << " - map    : " << elem << std::endl; });
  
  for (auto &elem : lazyseq) {
    std::cout << "[out] : " << elem << std::endl;
  }
実行結果:
[in]  : 3
 - filter : 3
 - map    : 6
[out] : 6
[in]  : 2
[in]  : 9
 - filter : 9
 - map    : 18
[out] : 18
[in]  : 6
なお、遅延評価では必要な時に処理されるため、 sort() だと少し注意が必要です。
ソートという処理を考えたらわかると思いますが、 ソートは全要素が揃わないと完了しない処理です。 そのため、 sort() に関しては流れるように次に渡すのではなく、 そこで一旦要素が溜められることになります。

今回紹介しているサンプルの多くは以下に記述しています。 こちらには sort() の確認用のコードも記述しています。


 
このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
31位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
5位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

08月 | 2023年09月 | 10月
- - - - - 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


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。