[ C++で開発 ]

可変引数

C/C++言語では、printf関数のように、引数の数が任意個数の関数を定義する方法があります。

C言語:stdargsによる実装

可変長引数を取る関数の定義

引数リストの末尾に、...で可変長引数の定義をします。

void log(Level level, const char* format, ...);

引数の取り込み: vsprintf系関数に渡す場合

引数の取り込みは、<cstdarg>をインクルードし、その中で定義される va_list, va_start, va_end マクロを使って行います。

void log(Level level, const char* format, ...) {
    va_list ap;
    va_start(ap, format);
    char* allocatedBuffer;
    int size = vasprintf(&allocatedBuffer, format, ap);
    va_end(ap);
    :
    free(allocatedBuffer);
}

va_listで、可変長引数オブジェクトを定義します。

va_startで、可変長引数オブジェクトに、関数の引数リストにおいて可変長箇所が始まる場所を定義します。上のコードでは、可変長部分は第2引数formatの次である第3引数以降なので、可変長部分の直前の引数 format を指定しています。

C言語のprintf関数のように、書式定義文字列とそれに引き続く可変長引数列を、書式に従って文字列化するシステム関数がいくつか用意されています。

  1. vsprintf
  2. vsnprintf
  3. vasprintf

1.のvsprintfは、あらかじめ確保したバッファより生成した文字列が大きくても、バッファに詰め込んでしまうので、メモリ破壊が生じます。2.のvsnprintfは、バッファサイズを越えて詰め込むことはありませんが、文字列が途中で切れてしまいます。3.のvasprintfは、システム関数側で必要なサイズのバッファをヒープメモリ上に確保するので文字列が切れることはありませんが、メモリの解放は呼び出し側で行う必要があります。

引数の取り込み: 自分で処理する場合

<cstdarg>の機能では、可変長引数の個数を取得することができません。

T.B.D.

可変長引数を取るマクロの定義

#define LOG_MESSAGE(level, fmt, ...) log(level, fmt, __VA_ARGS__)

マクロにおいても、関数同様可変長引数を...で定義できます。マクロ内でこの可変長引数を関数に渡す場合、可変長な引数をまとめて__VAR_ARGS__の名前で引き渡すことができます。

LOG_MESSAGE(DEBUG, "current size = %d", getSize());

可変長部分の引数がない場合の扱い

LOG_MESSAGE(INFO, "only fixed message");

としたいのですが、マクロが展開されたときに、log(INFO, "only fixed message",); と__VA_ARGS__がゼロ個の場合にカンマが残ってしまい、コンパイルエラーとなってしまいます。これは、__VA_ARGS__の箇所は最低1つの引数が必要という仕様によるものです。

可変長の引数が必ず1つはあるように定義する
#define LOG_MESSAGE(level, ...) log(level, __VA_ARGS__)

ただし、fmtで指定する引数がマクロ内で加工できなくなるので、高度なマクロを定義する際に困ることがあります。

GCC拡張のマクロ
#define LOG_MESSAGE(level, fmt, ...) log(level, fmt, ##__VA_ARGS__)

と##を付ける事によって、可変長引数部がゼロ個のとき、最後のカンマを除去してくれるGCC独自拡張機能を利用します。

ファイル名と行番号を出力に埋める

#define LOG_MESSAGE(level, fmt, ...) log(level, "%s:%d " fmt, __FILE__, __LINE__, __VA_ARGS__)

ちょっと特殊な書き方("%s:%d" fmt )をしています。コンパイル時に1つの文字列に結合可能なときは、このように文字列を並べて記述することができます。

例えば、LOG_MESSAGE(DEBUG, "result = %d", result); と記述すると、コンパイル(プリプロセス)時に、以下のように展開されます。

LOG_MESSAGE(DEBUG, "result = %d", result);
                 ↓
log(DEBUG, "%s:%d " "result = %d", __FILE__, __LINE__, result);
                 ↓
log(DEBUG, "%s:%d result = %d", __FILE__, __LINE__, result);

しかし、マクロに入れる書式指定文字列が実行時に確定する式の評価結果だとマクロ展開結果はコンパイル不能なコードになってしまいます。

LOG_MESSAGE(DEBUG, format.c_str(), result);
                 ↓
log(DEBUG, "%s:%d " format.c_str(), __FILE__, __LINE__, result);
                 ↓
           コンパイルエラー

C++的対応をするならば、

#define LOG_MESSAGE(level, fmt, ...) { \
    std::string format("%s:%d "); \
    format.append(fmt); \
    log(level, format.c_str(), __FILE__, __LINE__, __VA_ARGS__); \
}

GCC:__attribute__による実装