【ITニュース解説】Avoid the Temptation of bool
2025年10月04日に「Dev.to」が公開したITニュース「Avoid the Temptation of bool」について初心者にもわかりやすく解説しています。
ITニュース概要
関数パラメータに`bool`を直接使うと、その意味が不明瞭でコードの可読性が下がる。これを避けるため、機能を分けた複数の関数を作成するか、`enum`(列挙型)を使うことが推奨される。これにより、パラメータの意図が明確になり、理解しやすいコードにつながる。
ITニュース解説
プログラミング学習を進める中で、bool(多くの言語ではbooleanとも呼ばれる)という型に出会うことは多いだろう。これは「真(true)」か「偽(false)」かの二つの状態を表す、非常に基本的なデータ型だ。物事が「はい」か「いいえ」で表現できる場合に便利で、条件分岐の際などによく使われる。しかし、このbool型を関数の引数として使う際には、注意が必要だという話がある。安易に使うと、後々コードが読みにくくなり、思わぬ問題を引き起こす可能性があるため、その「誘惑」に注意しよう、というのが今回のテーマである。
例えば、メモリを確保するalloc_bufという関数があるとする。この関数を呼び出す際に、alloc_buf(4096, true);と書いたとしよう。引数の4096は確保するメモリのサイズだとすぐにわかるが、後ろのtrueは何を意味するだろうか。「確保したメモリをゼロで初期化する」という意味かもしれないし、「メモリ確保に失敗した場合にエラーメッセージを表示する」という意味かもしれない。「確保するメモリを特定のアドレス境界に揃える(アラインする)」という意味の可能性もある。このtrueを見ただけでは、その意図は全く伝わらないのだ。
このように、具体的な意味を持たないtrueやfalseが関数の引数として並んでいると、そのコードを読んだ人は必ず立ち止まってしまう。引数の意味を知るためには、その関数の定義(宣言)やドキュメントをわざわざ探して読まなければならない。これは、コードを読む人にとって大きな手間となる。プログラムは一度書いたら終わりではなく、何度も読み返され、修正され、拡張されていくものだ。特に、他の人が書いたコードを読む場合や、数ヶ月後に自分が書いたコードを読み返す場合には、コードの「わかりやすさ(可読性)」が非常に重要になる。引数の意味が不明瞭なboolは、この可読性を著しく低下させてしまうのだ。
では、boolを引数として使う代わりに、どのような方法があるのだろうか。いくつか代替案が考えられる。
一つ目の方法は、関数を二つに分けることだ。
先ほどのalloc_bufの例で言えば、「確保したメモリをゼロで初期化するかどうか」をboolで指定していたとする。これを、alloc_raw(size_t size)とalloc_zero(size_t size)という二つの関数に分けるのだ。alloc_rawは「初期化しない生(raw)のメモリを確保する」ことを、alloc_zeroは「ゼロで初期化されたメモリを確保する」ことを、それぞれの関数名自体が明確に示している。このようにすれば、関数を呼び出す側は、引数にtrueやfalseを指定する代わりに、どちらの関数を呼び出すべきかを判断するだけでよくなる。呼び出し例は、void* buf = alloc_zero(4096);となり、引数から意味を探る手間は一切なくなる。関数の内部実装としては、最終的に一つの共通関数を呼び出し、その共通関数にbool引数を渡す形にすることも可能なので、実装側の負担もそれほど増えない。
二つ目の方法は、列挙型(enum)を使用することだ。
列挙型とは、複数の関連する定数(名前付きの数値)をまとめた型のことである。例えば、「メモリの確保オプション」として、ALLOC_RAW(初期化なし)とALLOC_ZEROED(ゼロで初期化)という二つの選択肢がある場合、これらをenum alloc_opts { ALLOC_RAW, ALLOC_ZEROED };のように定義できる。そして、alloc_buf関数の引数にboolではなく、このalloc_opts型を使用するのだ。
関数の宣言はvoid* alloc_buf(size_t size, alloc_opts opt);となり、呼び出し方はvoid* buf = alloc_buf(4096, ALLOC_ZEROED);のようになる。こうすることで、ALLOC_ZEROEDという具体的な名前が「バッファをゼロで初期化する」という操作を直接的に表現し、引数の意味が格段にわかりやすくなる。これは、コードの可読性を大きく向上させる効果がある。
さらに、列挙型を応用して、ビットフラグとして使う方法もある。
これは、複数のオプションを同時に指定したい場合に非常に強力な方法だ。例えば、「メモリの初期化」だけでなく「アラインメント(特定のバイト境界に揃えること)」のオプションも追加したい場合、列挙型の各要素に異なるビットの値を割り当てる。enum alloc_opts { ALLOC_RAW = 0, ALLOC_ALIGNED = 1 << 0, ALLOC_ZEROED = 1 << 1, };のように定義する(1 << 0は1、1 << 1は2となる)。こうすると、各オプションがメモリ上の異なる「旗(フラグ)」に対応するイメージだ。
そして、関数を呼び出す際に、|(ビットごとのOR演算子)を使って複数のオプションを組み合わせることができる。例えば、void* buf = alloc_buf(4096, ALLOC_ALIGNED | ALLOC_ZEROED);のように書けば、「アラインされ、かつゼロで初期化されたメモリを確保する」という意図が明確に伝わる。この方法の大きな利点は、後から新しいオプションを追加したい場合でも、関数の引数の型(API)を変更する必要がなく、列挙型に新しい値を定義するだけで対応できる点だ。既存のコードは、新しいオプションを使わない限り再コンパイルするだけで動作し続けるため、高い拡張性を持つ。
三つ目の方法は、コメントを使用することだ。
これは、前述の二つの方法が使えない場合の「最終手段」と考えるべきだろう。例えば、自分が変更できないサードパーティ製のライブラリ関数や、何らかの理由で既存の関数のAPIを変更できない場合に有効だ。この場合でも、最低限、呼び出し側のコードにインラインコメント(コードと同じ行に書くコメント)を付けることで、可読性を向上させることができる。
void* buf = alloc_buf(4096, /*zero_buf=*/true);のように、引数の意味を表す名前をコメントで追記するのだ。これにより、コードを読んだ人がtrueがzero_bufを意味しているとすぐに理解できるようになる。完璧な解決策ではないが、何もしないよりははるかに親切なコードとなる。数ヶ月後の自分自身も、このコメントに助けられることになるだろう。
まとめると、bool型は非常に便利で手軽に使えるため、関数の引数として使いたくなる気持ちはよくわかる。特に、既存の関数にちょっとした機能を追加する際に、安易にbool引数を追加してしまう誘惑に駆られることは多い。しかし、コードの明確さや可読性は、長期的な開発やメンテナンスにおいて非常に大きな価値を持つ。曖昧なbool引数を持つコードは、将来的に多くのバグや混乱の原因となる可能性がある。
システムエンジニアを目指す上では、いかに「わかりやすい」コードを書くかという視点が非常に重要だ。機能を細かく分けた別の関数を用意する、または意味のある名前を持つ列挙型を導入する、といった工夫をすることで、より高品質でメンテナンスしやすいプログラムを構築することができる。安易なbool引数の使用を避け、少し手間をかけてでも、意味が明確に伝わるインターフェースを設計することを心がけることが、良いプログラマーへの第一歩となるだろう。