C言語の関数の特殊化

341, 2021-11-10

目次

C言語の関数の特殊化

引数爆発が起こった関数は使いづらくなる。
開発者はそういう関数にはあいそを尽かして使わなくなる。

どうするのかというと、そういう時は関数を特殊化する。
特殊化とは、関数を特定のモジュール用に再定義することを指す。

この時、おおもとになる関数と、それを利用し特殊化を行う関数とに分かれる。
おおもとになる関数は引数が多いが、特殊化を行った関数は引数が限定されるので、結果的に使いやすくなる。

たとえば以下のようなモジュールAの関数を考えてみる。

/**
 * keyのオブジェクトをcontextから検索する
 * globalがtrueの時、グローバルなコンテキストで検索を行う
 */
void find_object(Context *context, const char *key, bool global);

この関数は見た感じ、十分使いやすく見える。
(引数をたくさん書くのが嫌なんだ)

で、この関数を使うモジュールBがあるとする。
モジュールBではオブジェクトを検索するときにグローバルなコンテキストから検索をしない。
そのためfind_object()を使う時、global引数は必ずfalseになる。

global引数を毎回falseにするのは、決まり切っているのになんだかばかばかしい。
こういう時はそのモジュール内でヘルパーやユーティリティーを用意するのが普通だ。

たとえば↓のような関数だ。

void find_object_module_a(Context *context, const char *key) {
    return find_object(context, key, false);
}

このように特定のモジュール向けに関数を新調することを私は関数の特殊化と呼んでいる。
このfind_object_module_a()はどこに置かれるか。
この関数はモジュールAのヘッダに置かれる。
もちろんモジュールBのヘッダでもいいし、モジュールBのソース内にstaticで定義してもいいだろう。

わざわざ引数を省略するために関数を作るなんて大げさだ、と思うかもしれない。
しかし、C言語において関数の引数は馬鹿にしてはならない。
関数の引数が増えれば、それだけメンテナンスが大変になる。

たとえば先ほどのfind_object()関数の引数が増えたとしよう。
そうすると、この関数を使っているコードを修正していく必要が出てくる。
コンパイラが問題のコードを検出してくれるので、修正ははかどるだろう。
だが、プロジェクトの規模が大きくなれば、それは無視できないコストになる。

関数の引数を1つ追加しただけで1週間も作業がとまりました。
なんてことがザラになるだろう。おそろしい。
時間は有限なので、節約しなければいけない。
余計な事は極力減らしたい。

find_object_module_a()を定義しておけば、少なくとfind_object()の変更の影響はこの関数内に限定されることになる。
find_object_module_a()内のコードをちょろっと変えればそれで完了だ。

もしモジュールB内でfind_object()をそのまま使っていたら、find_object()を使っているコードを1つずつ変更しなければいけない。
置換でいけるだろうか?置換は万能ではないし、バグを挿入するリスクもある。
ということは、こういった関数を特殊化しておいて仕様変更に備えるのは良い判断だということになる。

基本的に特殊化した関数は増えてもそれほど問題にはならない。
それは仕様変更のクッションとしてそこに存在するからだ。
クッションがいくらあっても大した問題にはならない。
ただし名前は重要だ。

さきほどのfind_object_module_a()は以下のような関数として定義しておいてもいいだろう。

void find_object_local(Context *context, const char *key) {
    return find_object(context, key, false);
}

このような関数を作っておけば、この関数はモジュールBからも解放され、モジュールAの新しい戦力として加えることができる。
ようするにラッパーって便利だよね、という話だが、ラッパーの重要性はプロジェクトの規模が大きくなればなるほど重要になる。
ラッパーを積極的に使って関数を特殊化していこう。