Programming Field - プログラミング Tips

C++の参照渡し引数直後の「...」

C++では、ポインタの他に「参照」が存在し、引数でもこれを指定することができます。参照型の特徴の一つに、参照型の変数に「&」を付けてポインタを取得しようとすると、その参照型変数が指しているオブジェクトのポインタを得ることができます。一方、C/C++では関数の引数に「...」を置くことで、可変長の引数を取る関数を作成することができます。

この2つを組み合わせて以下のようにコードを書くと、(少なくともx86系プロセッサでは)うまく実行できません。

// メモリオブジェクトを自分で管理するクラス
// (クラスインスタンスとメモリオブジェクトが1対1の対応になるようにしている)
class CTest
{
public:
    CTest() { ... }
    CTest(const CTest& obj) { ... }
    ~CTest() { ... }

    int len;
    char* buffer;
};

// CTest::buffer を書き換える関数
void __cdecl TestFunc(CTest& obj, ...)
{
    int i;
    va_list ap;
    va_start(ap, obj);
    for (i = 0; i < obj.len; i++)
        obj.buffer[i] = va_arg(ap, char);
    va_end(ap);
}

このコードにおける問題の原因は、va_startにあります。x86系プロセッサで話をすると、va_startはapを初期化する際、普通はva_startの第二引数に指定された変数の「アドレス」を取得し、可変長引数を取る関数の「引数リスト」ポインタを取得します。

例えば、

void __cdecl MyFunc(int count, ...)
{
    va_list ap;
    int val;
    va_start(ap, count);
    val = va_arg(ap, int);
    ...
    va_end(ap);
}

というコードでは、va_startの部分で「&count」を計算し、va_argでは最終的に「*(((int*) &count) + 1)」(または「((int*) &count)[1]」)のようにして「引数リスト」の次の項目を取得しています。ここで、「&count」が指す値はスタック上にある「引数リスト」のポインタになっているという前提があります。

そこで最初のコード例を同様に解釈すると、va_startの部分で「&obj」を計算し、「引数リスト」のポインタを取得しようとするのですが、objは参照型であるため、「&obj」はobjが実際に指しているオブジェクトのポインタが返ることになります。したがって、va_startは「引数リスト」のポインタを取得しようとして別のポインタを取得してしまっているので、va_argでの結果は未定義となります。

この問題を回避するためには以下のような対策が考えられます。

  1. 参照型の引数の直後が「...」にならないようにする
    • 「...」の直前の引数が参照型でなければこのような問題は発生しません(※「&」をoperatorでオーバーライドしているクラス型を除く)が、他に引数が無い場合、無駄な引数を1つ作ることになります。
  2. 参照型を使わない
    • 例えば最初のコード例では「CTest&」を「CTest」に変えます。しかしクラスの中身を関数内で操作する場合は意図した動作を得られない場合があります(上記の例はそれに当たります)。
  3. 可変長引数を取るようにしない(「...」を使わない)
    • C++ではオーバーライドがあったり配列クラスがあったり(作成できたり)するので、「...」を使わないで事足りる場合がほとんどですが、標準C関数のvprintfなどを呼び出す場合は「...」が必要になります。
  4. アセンブリを使って「引数リスト」を取得できるようにする
    • 「&obj」では取得できなくても、スタック上にはきちんと「引数リスト」が存在するので、アセンブリで「引数リスト」を取得し、objと同じ位置に当たる「引数リスト」中のポインタを得れば出来ますが、多少複雑な上アセンブリを直接書く必要があり、さらにはこの時点で機種依存になってしまうのであまりお勧めできません。

C++においては3つ目が一番いいのではないかと思われますが、時と場合に応じて…。

最終更新日: 2009/03/03