C言語の関数の引数が多くなった時の対処方法【引数爆発】

364, 2021-12-09

目次

C言語の関数の引数が多くなった時の対処方法

C言語の関数は引数を取れます。
引数は複数設定することができます。
たとえば↓のようにです。

void func(int a, float b) {
    // ここに処理
}

↑のコードで言うとint afloat bが引数です。
この引数は呼び出し側で見たときに「実引数」、関数側で見たときに「仮引数」と呼ばれます。
実引数と仮引数をまとめて「引数」と呼びます。

今回はこの引数が多くなってしまったときの対処方法を備忘録的に書きたいと思います。

C言語の関数の引数爆発

bomb

「引数爆発」とは、関数の引数がとても多くなってしまい、管理が大変になっていることを言います。
アーギュメンツ・エクスプロード……英語にすればかっこいいですが、できれば相手にしたくない状態です。

具体的にコードを見てみましょう。
まず普通の関数です。

void good(int a, float b) {
    // ここに処理
}

このgood関数は健康状態のいい優れた関数と言えます。
引数がコンパクトで、関数名も短くて使いやすいです。
もっとも関数名については短ければ良いというものでもありませんが。

では健康状態の悪い関数を見てみましょう。

void bad(
    int a,
    float b,
    double c,
    const char *d,
    struct stat *e,
    Article *f,
    Hige *g
) {
    // ここに処理
}

見てくださいこのbad関数を。引数がとんでもないことになっています。
この状態はまさに引数爆発です。見るも無残です。
この関数を呼び出す場合は↓のようなコードを書く必要があります。

bad(0, 1.2, 3.4, "str", &st, &article, &hige);

なんというか、この関数がなにをやりたいのかさっぱりわかりませんよね。
これはひどい。見るも無残です。

このbad関数を使うには毎回7つもの引数を設定しなくてはいけません。
関数を1つ使うたびに7回分の思考が必要になると言ってもいいかもしれません。
非常にコスパのわるい関数です。まさにbadと言えるでしょう。親の顔が見たいくらいです。

親はお前だ

引数爆発を起こした関数のメンテナンス性

maintenance

引数爆発を起こした関数のメンテナンス性はどうでしょうか?
意外ですが、手間がかかる割にはメリットもあります。

関数の引数を変更した場合は、関数を呼び出しているコードも変更が必要になります。
これはコンパイルレベルで検出できるので、メリットと言えます。
コードを変更する手間があるわけですが、あやまってバグを埋め込む可能性は低くなるのでメリットと言っても良いと思います。
引数爆発はデメリットばかりでなくこういったメリットも意外にあるわけです。

これは後述する構造体を使った引数の抽象化では得られないメリットです。
また構造体を使った方法はコンパイルレベルで変更を検出できないというデメリットも抱えています。

引数爆発をなんとかする

引数爆発を何とかするには2種類の方法があります。
それは↓です。

  • 局所化

  • 構造体にまとめる

「局所化」とは、関数を用途ごとに局所化して、ラッパーを作ることを言います。
「構造体にまとめる」とは、関数の引数を構造体にまとめることを言います。
まず局所化から見てみたいと思います。

局所化

wrapper

局所化の例を見てみましょう。
さきほどのbad関数のラッパーを作ります。
名前はbad_wrapper()です。

void bad_wrapper(struct stat *st, Article *article, Hige *hige) {
    bad(0, 1.2, 3.4, "str", st, article, hige);
}

bad_wrapper()は内部でbad()を呼び出していますが、引数は3つだけです。
bad_wrapper()は内部ではbad()のいくつかの引数を適当に設定しています。
これが局所化です。

bad_wrapper()は特定の用途に限定されて使われます。
bad()のいくつかの引数を勝手に設定しているので、自然に用途は限定されるわけです。
bad_wrapper()という名前はわかりづらいので、↓のようなラッパーを作ってみましょう。

// many ... 多い
void bad_many(struct stat *st, Article *article, Hige *hige) {
    bad(1000, 1234.5, 6789.0, "many", st, article, hige);
}

// few ... 少ない
void bad_few(struct stat *st, Article *article, Hige *hige) {
    bad(1, 1.2, 1.2, "few", st, article, hige);
}

↑の関数bad_many()を設定値を多くしたい時に使われます。
いっぽうbad_few()は設定値を少なくしたい時に使われます。
それぞれ、用途が違う関数ですが、内部で呼び出しているのはともにbad()です。

このように関数の用途を限定して、局所化したラッパーを作ることで引数爆発を抑えることができます。

構造体にまとめる

structure

構造体にまとめるとは、そのままですが関数の引数を構造体にしてしまう方法です。
先ほどのbad()関数の引数用の構造体を作ります。

struct bad_args {
    int a;
    float b;
    double c;
    const char *d;
    struct stat *e;
    Article *f;
    Hige *g;
};

void bad(struct bad_args *args) {
    // ここに処理
}

↑のようにbad()関数の定義は非常にシンプルになります。
呼び出し側のコードを見てみましょう。

bad(&(struct bad_args) {
    .a = 1,
    .b = 2.3,
    .c = 4.5,
    .d = "str",
    .e = &st,
    .f = &article,
    .g = &hige
});

なんだ、まだ爆発してるじゃないか。
はい、その通りですね。
構造体を使った場合、引数をすべて設定すると↑のように依然として引数爆発の状態になります。

しかしこの方法はいくつかの引数を省略することができます。

bad(&(struct bad_args) {
    .e = &st,
    .f = &article,
    .g = &hige
});

↑の呼び出し側のコードでは、aからdまでの引数を省略しています。
構造体のテンポラリオブジェクトの作成では、省略した他の引数は0クリアされます。
いわゆるキーワード引数や可変長引数のような使い方できるわけです。
構造体にまとめる方法は関数の定義側の引数はシンプルになりますが、呼び出し側では依然として引数の指定が必要になります。

またこの方法には問題もあります。
たとえばbad()の引数を追加した場合を考えてみます。
この場合、bad_args構造体にメンバを追加すればそれで完了です。
一見すると簡単に見えますが、問題は呼び出し側のコードです。

この方法では引数の増減をコンパイルレベルで検出することができません。
そのため、bad()を使っているコードの修正漏れが発生する場合があります。
つまり、bad()を使っているコードを変更しなくてもコンパイルが通ってしまうので、コンパイルエラーを活用した修正ができないということです。

コンパイルエラーは敵でしょうか? 味方でしょうか?
実行時のエラーを減らせるという点で、コンパイルエラーは我々の味方と言えます。
この引数を構造体にまとめる方法は、このコンパイルエラーを封殺してしまうデメリットがあります。

おわりに

end

今回は関数の引数が多くなってしまったときの対処方法について解説しました。
局所化できる場合は局所化したほうがメンテナンスが楽になるでしょう。
構造体にまとめる場合はそのデメリットをよく把握して使ってください。

筆者は両方の手法をプロダクトで使っています。
構造体にまとめる方法はかなりクセのある方法ですが、局所化は使い勝手が良いです。
以上です。