munepi.com > Article > 可変個引数の落とし穴

可変個引数の落とし穴

引数の型に気を付けろ!

以前、garrayという、可変個引数とテンプレートを使った任意次元の動的配列クラスを紹介しました。

次元の数=引数の数を仮定したくないために、可変個引数を使っていたのですが、いやいや気を付けないと危険です。

#include <gmgarray.h>

typedef moo::garray<int, 2> iarray2;

int foo(iarray2& a, int x, int y)
{
    return a(x, y); ... (1)
}
int bar(iarray2& a, double x, double y)
{
    return a(x, y); ... (2)
}

実は(1)も(2)も不正です(!)。理由は、garray::operator()へ渡す引数は garray::size_type型(unsigned intのtypedef) でなければいけないからです。

型チェックは信用だけ

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; ... (3)

    va_start(arg, first); ... (4)
    for (size_type i = 1; i < dim; i++) {
        pos += _offset[i] * va_arg(arg, size_type); ... (5)
    }
    va_end(arg); ... (6)

    return _value[pos];
}

(3)で、可変個引数変数(?)を宣言します。 実際は現在のスタックの位置を示すポインタ(でいいはず)です。続いて(4)でポインタを初期化します。 最初の引数(\var{first})を与えることで、 \var{first}の次を指す値が\var{arg}にセットされます。ここまではどんな場合でも全く曖昧さはありません。

しかし、問題は次からです。

(5)では、現在ポインタが指している場所に size_type型の値が格納されているとみなして値を取得し、 そしてsizeof(size_type)だけポインタを進めます

つまり、このとき\var{first}以下の可変個引数部分に渡された変数が size_type以外のものであった場合は、全く予期しない結果になってしまうのです。

int型の値を渡したときには、 その値が正である限りは問題は表面化しないでしょう。 正のintはunsigned intと区別できないからです。しかし、負の値になった場合はunsigned intと見なした場合には とても巨大な(正の)値と見なされてしまうので、範囲外のアドレスをアクセスするというバグになることは確実です。まぁ、通常の使用方法であればintを渡しても構わないかも知れません。

厳密にしたい場合は、

int main(int argc, char* argv[])
{
    iarray2 a;
    cout << foo(a, 2u, 3u);
}

とやっておけばOKです。

引数の型のサイズに注意せよ

もし、(2)のようにdoubleを渡した場合はもっと深刻です。doubleは8バイトあるため、 unsigned intと見なしてしまうと下位4バイト分と上位4バイト分が別々の値として評価されてしまいます。結果、とんでもない値になったりアクセス違反で落ちたりすることがあるでしょう。

floatは4バイトですが、 unsigned intとは内部形式が全く違うので正しい値を得られません。

charやshortなど、 unsigned intより少ないバイト数の変数は、 引数として渡されるときに4バイトに自動的に拡張されて渡されます。ですので、元の型が符号付きだったらintに、 符号なしだったらunsigned intとして渡されることになります。この場合は、intの時の問題点と同様に考えればいいでしょう。ただし、charは符号の扱いが処理系依存なのでより面倒です。

いずれも渡したい値が正の場合はキャストをすれば解決します。

int bar(iarray2& a, double x, double y)
{
    return a((iarray2::size_type)x, (iarray2::size_type)y);
}

元々が負の場合は、キャストをするとintと同じ問題が生じますが、 負のインデックスを渡すと言うこと自体がバグであるはずなので、 ここは目をつむることにしましょう。

# 添え字演算子[]には負の値を入れられるから、そういう使い方ができると便利ではあるが・・・。