可変引数リストを使った任意長のベクトル初期化

C/C++ の可変引数リスト (variable argument list) をあまり使ったことがなかったので,練習も兼ねて,初期値付きで任意長のベクトルとかリストを生成できる関数を作る.例えば

list<double>  x= container_gen<list<double> >(5, -5.0,10.0,3.0,1.0,2.0)

のような感じで,サイズ5の list 型を生成し,それが [-5.0, 10.0, 3.0, 1.0, 2.0] の要素を持つようにする.
可変引数リストを持つ関数の宣言は

list<double>  container_gen (int size, ...);

のように,可変の引数部分を ... で指定する.この可変の部分を取り出すには, cstdarg をインクルードする.このライブラリでは va_list 型と va_start, va_arg, va_end 関数などが定義されていて,これらを用いることで可変引数を利用できるようになる. printf 関数なども可変引数リストを用いて実装されている.

具体的には,

  va_list  argptr;

で va_list 型の可変引数オブジェクトを定義し,

void va_start (va_list argptr, last);

関数(実はマクロらしい)で argptr を初期化する.ここで last はもとの引数リストのうち,可変部分の直前の引数の識別子を指定する(以下の例参照).

type va_arg (va_list argptr, type);

この関数は既に読みだした可変引数リストの次の可変引数を type 型として取り出す. va_start 直後だと最初の可変引数が取り出され,以後,この関数を呼ぶたびに順に可変引数が取り出されていく.何番目を取り出しているかは argptr に記録される. argptr は必ず va_start で初期化したものでないとならない.

void va_end (va_list argptr);

この関数は va_start に対応させて使わなければならない.後処理をする.

これらを使うと,任意長の初期値付きベクトル生成は,

list<double>  container_gen (int size, ...)
{
  va_list argptr;  // 可変引数オブジェクト
  va_start (argptr,size);  // argptr を初期化する

  list<double> res(size,0.0);
  for (typename list<double>::iterator w itr (res.begin()); itr!=res.end(); ++itr)
    *itr= va_arg (argptr, typename container::value_type);

  va_end (argptr);
  return res;
}

のように実装できる.注意しておきたいのは, va_start の第2引数が 可変引数の「数」ではなく,可変引数の直前におかれた引数(... の直前におかれた引数)の名前を指定する必要があることだ.つまり,ここでは size だ. res は戻り値の list 型である.不思議な感じのするコードだが,実装じたいはシンプルだ.

テンプレートを使って実装した例 (vector,list 用)

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
#include <cstdarg>
namespace hogehoge
{
//!\brief vector, list などを生成する (container generator)
template <typename container>
container  container_gen (int size, ...)
{
  va_list argptr;
  va_start (argptr,size);

  container res(size,0.0);
  for (typename container::iterator  itr (res.begin()); itr!=res.end(); ++itr)
    *itr= va_arg (argptr, typename container::value_type);

  va_end (argptr);
  return res;
}
} // end of namespace hogehoge

using namespace std;
using namespace hogehoge;

template <typename T>
void _print (const T &val)
{
  cout<<" "<<val;
}
//!\brief container 表示関数
template <typename container>
void print_container (const container &val)
{
  for_each (val.begin(), val.end(), &_print<typename container::value_type>);
}
#define cprint(var) {cout<<#var"= "; print_container(var); cout<<endl;}

int main(int argc, char**argv)
{
  cprint (container_gen<vector<double> >(2, 1.0,2.0));
  cprint (container_gen<vector<int> >(7, 4,3,2,1,2,3,4));
  cprint (container_gen<list<double> >(5, -5.0,10.0,3.0,1.0,2.0));
  return 0;
}

と実行結果:

container_gen<vector<double> >(2, 1.0,2.0)=  1 2
container_gen<vector<int> >(7, 4,3,2,1,2,3,4)=  4 3 2 1 2 3 4
container_gen<list<double> >(5, -5.0,10.0,3.0,1.0,2.0)=  -5 10 3 1 2

ひとつだけ注意. main 関数で

  cprint (container_gen<vector<double> >(2, 1,2.0));

のようにすると(可変引数の最初が整数定数),

container_gen<vector<double> >(2, 1,2.0)=  4.94066e-324 4.9422e-270

という結果になる.これは,整数型 (int) をムリヤリ double 型で読もうとした結果だろう.

liboctave へん

次のサンプルでは ColumnVector, RowVector 用に octgen1(size,...) を, Matrix 用に octgen2(rows,cols,...) を定義している.

#include <iostream>
#include <octave/config.h>
#include <octave/Matrix.h>
#include <cstdarg>  // 可変長引数リストのため
namespace hogehoge
{
//!\brief ColumnVector, RowVector などを生成する
template <typename oct_type>
oct_type  octgen1 (int size, ...)
{
  va_list argptr;
  va_start (argptr,size);

  oct_type res(size,0.0);
  typename oct_type::element_type *pres (res.fortran_vec());
  for (int i(0);i<size;++i,++pres)
    *pres= va_arg (argptr,typename oct_type::element_type);

  va_end (argptr);
  return res;
}
//!\brief Matrix などを生成する
template <typename oct_type>
oct_type  octgen2 (int rows, int cols, ...)
{
  va_list argptr;
  va_start (argptr,cols);

  oct_type res(rows,cols,0.0);
  for (int r(0);r<rows;++r)
    for (int c(0);c<cols;++c)
      res(r,c)= va_arg (argptr,typename oct_type::element_type);

  va_end (argptr);
  return res;
}
} // end of namespace hogehoge

using namespace std;
using namespace hogehoge;
#define rvprint(var) std::cout<<#var"= "<<(var)<<std::endl
#define cvprint(var) std::cout<<#var"= "<<(var.transpose())<<std::endl
#define matprint(var) std::cout<<#var"= "<<endl<<(var)
int main(int argc, char**argv)
{
  cvprint(octgen1<ColumnVector>(2, 1.0,2.0));
  cvprint(octgen1<ColumnVector>(5, -5.0,10.0,3.0,1.0,2.0));
  rvprint(octgen1<RowVector>(3, -10.0,1.0,2.0));
  matprint(octgen2<Matrix>(2,3, -5.0,10.0, 3.0,1.0, 2.0,2.0));
  return 0;
}

結果:

octgen1<ColumnVector>(2, 1.0,2.0)=  1 2
octgen1<ColumnVector>(5, -5.0,10.0,3.0,1.0,2.0)=  -5 10 3 1 2
octgen1<RowVector>(3, -10.0,1.0,2.0)=  -10 1 2
octgen2<Matrix>(2,3, -5.0,10.0, 3.0,1.0, 2.0,2.0)=
 -5 10 3
 1 2 2

所感

実引数の型と va_arg で読み出す型が一致していないと,期待した結果が得られない.しかもこれはコンパイルの段階でわからず(型付き言語なのに),実行してみて始めて,何かおかしいことに気づくのだ.多用すると見付けにくいバグの原因になりそうな予感がする.