可変個引数入門 & garray
プログラム中で配列を、それも可変サイズの配列を扱う需要はとっても多いもの。C++の組み込み配列では不便なので、自前でいろいろ工夫している人も多いと思います。
私もテンプレートと可変個引数を組み合わせることで任意の次元数・任意のサイズを扱える、 garrayというクラスを作って使っています(gmgarray.h)。
ポイントは次の部分。
template <class T, unsigned int _dim>
class garray
{
public :
...
bool allocate(size_type first, ...);
T& operator()(size_type first, ...);
...
};
テンプレート引数で型Tと次元_dimを指定します。次元は1なら線、2なら平面、3なら空間、4なら・・・と言う意味で使ってます。
そして、各次元の長さをallocateで指定してメモリを確保するのですが、 ここでは可変個引数を使っています。allocateやoperator()の宣言の、...と言う部分がそうです。見慣れない人もいるでしょう。
可変個引数はprintfで有名ですが、 呼び出し側で必要なだけ引数を書き連ねることができ、 関数側では、なんらかの取り決めを元に引数をいくつか取り出し処理するというものです。
printfの場合は%dとか%sとかの制御文字が引数の数を決め、 garrayの場合は次元数が引数の個数になります。
allocateやoperator()を可変個引数で定義することで、 _dimの値に関係なく、同じソースコードで対応できるというのが最大の利点です。テンプレートの良さを生かすことができます。
一方の欠点としては、コンパイラで検出できないエラーを誘発するということがあります。
引数は、型も個数もprintfで言えば制御文字でだけ決まります。だから、正しく書式を書いて、この取り決めを呼び出し側・関数側が守らないとメモリ参照が食い違ってしまい、 バグの温床となるのです。
特に問題なのはこの点はコンパイラでは検出できないと言うことです。昨今の型を厳密にする流儀に反します。 また、型を厳密にする機能を持つテンプレートとも相反しています。
そういうわけで、今のプログラミングの潮流的には、 可変個引数というのはgotoと並んで避けるべきものと思われているような気がしますが、 それが最もうまくいく方法ならば(gotoも含め)私は使うべきと考えています。
garrayでは次のように使っています。
template <class T, unsigned int _dim>
garray<T, _dim>::reference garray<T, _dim>::operator()(size_type first, ...)
{
size_type pos = _offset[0] * first;
va_list arg;
va_start(arg, first);
for (size_type i = 1; i < _dim; i++) {
pos += _offset[i] * va_arg(arg, int);
}
va_end(arg);
return _value[pos];
}
可変個引数の仕組みは、次の通りです。
引数ポインタ変数に(arg) 最初の可変個引数のアドレスを取得して(va_start)、 そのアドレスをある型と「みなして」値を取得し(va_arg、ここではintとみなしている) 最後にポインタを破棄します(va_end)。
引数リストには、可変個引数の開始位置を知るために、 必ず一つ以上の通常の引数(ここではfirst)が必要です。
garrayでは、引数は各次元における位置になります。引数を可変個にすることで、_dimの値によらず一つのソースコードで対応しています。
さてこのgarrayですが、こんな風に使えます。
void foo() {
using namespace moo; // 名前空間mooの使用を宣言
// garrayはmooの中に入っているため
garray<int, 3> a; // 3次元の配列を使う
a.allocate(100, 100, 100); // 各次元の長さを100にして初期化
for (int z = 0; z < 100; z++) {
for (int y = 0; y < 100; y++) {
for (int x = 0; x < 100; x++) {
a(x, y, z) = x + y * 100 + z * 10000;
}
}
}
cout << "内容は.";
for (int z = 0; z < 100; z++) {
for (int y = 0; y < 100; y++) {
for (int x = 0; x < 100; x++) {
cout << a(x, y, z) << " ";
}
}
}
}
他にも、STL互換のbegin(),end()関数も備えており、 一部のSTLアルゴリズムが適用可能です(foreach()とか、transform()とか)。
さて、配列が使いやすくなってくると行列計算とかにも応用してみたくなりますが、 それはまた次回。