C 言語 マクロ講座 # 実用編2007年03月19日 11時31分44秒

# の使い方を具体例をあげてみよう。

logger の仕様は次の通りだ。set_level によって任意の警告レベルの出力の有無を制御できる。設定できる警告レベルは事前に決められている。各出力には、それぞれの警告レベルを示す文字列が含まれる。警告レベルは、debug、info、warning、error、critical がある。

列挙型を使った関数を書く時に良く見るコードは大抵以下のように、列挙型を switch に丁寧に並べてある事が多い。


% cat logger.c
enum level
{
    debug,
    info,
    warning,
    error,
    critical,
    end, /* marker */
};

int levels[] =
{
    0, /* debug */
    0, /* info */
    0, /* warning */
    1, /* error */
    1, /* critical */
};

int logger(enum level code, char *message)
{
    if(code < 0 || code >= end)
        return 0;
    if(!levels[code])
        return 0;
    switch(code)
    {
        case debug:
            return printf("debug: %s\n", message);
        case info:
            return printf("info: %s\n", message);
        case warning:
            return printf("warning: %s\n", message);
        case error:
            return printf("error: %s\n", message);
        case critical:
            return printf("critical: %s\n", message);
        default:
            return 0;
    }
}

int set_level(enum level code, int display)
{
    switch(code)
    {
        case debug:
            levels[debug] = display;
            return 1;
        case info:
            levels[info] = display;
            return 1;
        case warning:
            levels[warning] = display;
            return 1;
        case error:
            levels[error] = display;
            return 1;
        case critical:
            levels[critical] = display;
            return 1;
        default:
            return 0;
    }
}

int main()
{
    logger(error, "error message 1");
    logger(info, "info message 2");
    set_level(info, 1);
    logger(info, "info message 3");
    logger(error, "error message 4");
}

確かに動く。文法的にも全く問題ない。もし、このまま仕様が変わることがなければ、わざわざ書換える必要も無かろう。

しかし、新たに email という警告レベルを入れてくれと言われたら、このプログラムの作法に則って email を入れるよりも、さっさと全てを書き直した方が、よっぽど早い。


% cat log2.c
enum level
{
    debug,
    info,
    warning,
    error,
    critical,
};

#define LOG(name, init) name, #name, init

struct
{
    enum level code;
    char *name;
    int display;
} levels[] =
{
    LOG(debug, 0),
    LOG(info, 0),
    LOG(warning, 0),
    LOG(error, 1),
    LOG(critical, 1),
};

int logger(enum level code, char *message)
{
    if(code < 0 || code >= sizeof(levels)/sizeof(levels[0]))
        return 0;
    if(levels[code].display)
        return printf("%s: %s\n", levels[code].name, message);
    return 0;
}

int set_level(enum level code, int display)
{
    if(code < 0 || code >= sizeof(levels)/sizeof(levels[0]))
        return 0;
    levels[code].display = display;
    return 1;
}

int main()
{
    logger(error, "error message 1");
    logger(info, "info message 2");
    set_level(info, 1);
    logger(info, "info message 3");
    logger(error, "error message 4");
}

列挙型と配列をマクロを有効に使って組み合わせた為に、元と比べたら、変更に柔軟に対応できる様になった。いくつも繰り返し使われていた、警告レベルが一つの場所で使われるだけになったのだ。残念ながら、列挙型と配列に二度同じ値を並べるのは避けられない。列挙型を宣言し、そこで宣言された新しい型を定義するのが、各一回ずつ必要だからだ。

ここで、マクロが使われたのは、文字列を生成する為だけだ。確かに、マクロで文字列を生成しないで、直接書き込んでももちろん同じように動作する。そういった意味ではマクロを有効に使っている様には見えないかもしれない。しかし、同一行に同じ文字を二回打つのを苦にしない人は、三回、四回と何度打っても全く苦にしないのである。そのような人は結局のところ、配列を効率良く使ってみようなどという発想自体が出てこないのである。

動作の結果だ。


% make log
cc -O2 -fno-strict-aliasing -pipe   log.c  -o log
% ./log
error: error message 1
info: info message 3
error: error message 4
% make log2
cc -O2 -fno-strict-aliasing -pipe   log2.c  -o log2
% ./log2
error: error message 1
info: info message 3
error: error message 4

前回次回