C
C++
CI

機能を切り替えるための #ifdef は最小限にしよう

はじめに

C / C++ における #ifdef (および #if) による条件付きコンパイルは強力な道具ですが、使いすぎると非常にメンテナンス性の悪いコードになってしまいます。諸刃の件です。
ここでは主に継続的インテグレーションの観点から、 #ifdef で切り替えられるコードは最小限になるよう工夫しましょう、ということを整理してみます。

継続的インテグレーションと #ifdef

現代のソフトウェア開発では CI 環境を整え、常にビルド・結合・自動テストや静的解析を実行し続けるのが普通です(よね ?)。#ifdef によって無効になったコードはこのチェックから外れてしまい、その恩恵を受けられません。

もちろん、全てのコードを網羅するように複数の条件でコンパイルし、それぞれチェックを走らせることも可能ですが、条件が多くなってくるとそのコストは大きくなっていきます。少なくとも CI の観点からは、#ifdef による条件コンパイルは最小限に留め、できるだけたくさんのコードがコンパイル・テストされている状態にしたほうがよい、と考えられます。

少しでもコンパイル・テストされるコードを増やす例を書き出してみます。

動的な(実行時の)切り替えで代替できないか

まず単純に、 コンパイル時に切り替えるのではなく、実行時の切り替えでもよいかもしれません。

  • 関数のパラメータやオブジェクト生成時のパラメータとして付与し、それによってふるまいを変える
  • 複数のふるまいや複数の実現手段を、あるインターフェイスを実装する複数のクラスとして表現し、どのクラスを生成するかで切り替える (strategy pattern)

といった方法が取れるなら、すべてのコードがコンパイルされますし、テストコードでテストすることもできます。

条件コンパイルに比べコードサイズや実行時のオーバヘッドは生じるので、それを許容できるかの判断はもちろん必要です。

無理やり普通の if にする

例えば

#ifdef FEATURE_FOOBAR
// FOOBAR 用のコード
#else
// それ以外用のコード
#endif

みたいなコードをばらまくのではなく、どこか一カ所で

#ifdef FEATURE_FOOBAR
constexpr bool kFoobarSupported= true;
#else
constexpr bool kFoobarSupported= false;
#end

としておいて

if (kFoobarSupported) {
  // FOOBAR 用のコード
} else {
  // それ以外用のコード
}

にする方法があります。
まず第一に読みやすいですし、「FOOBAR 用のコード」「それ以外用のコード」どちらの条件のコードもコンパイル対象になります。コードサイズや実行時コストについては、if () の条件が静的に決まるのであれば、コンパイラの最適化で使わないコードが削られることは期待してよいと思います。
ただし、これだけだと無効になっているコードのテストはできません。

テンプレートパラメータとして付与する

複数の条件のコードを両方ともテストしたい、しかし動的な切り替えのオーバーヘッドが許容できない場合、こういうのもあります。

template <bool FoobarSupported>
void do_something_impl() {
  if (FoobarSupported) {
    // FOOBAR 用のコード
  } else {
    // それ以外用のコード
  }
}
#ifdef FEATURE_FOOBAR
void do_something() { do_something_impl<true>(); }
#else
void do_something() { do_something_impl<false>(); }
#endif

こんなふうにすることで、 do_something_impl<true>() / do_something_impl<false>() をテストすれば両方のコードをコンパイル・テストすることが可能になります。
実際に使われるのは片方だけ、しかもオーバーヘッドはゼロです。

クラスの typedef で切り替え

クラス単位の簡単な切り替えです。
仮想関数を使った動的なディスパッチのコストが許容できない場合に、ふるまいの違うクラスを複数用意しておいて

#ifdef FEATURE_FOOBAR
using Widget = FoobarWidget;
#else
using Widget = BazWidget;
#endif

のようにすることで、 FoobarWidgetBazWidget の両方のコードをコンパイル・テストはしつつ、実際に使うのは片方だけ (利用者は Widget を使う)、ということができます。

#ifdef が避けられないケース

  • OS の違いやコンパイラの違いを吸収するため。
  • いわゆるリリース/デバッグビルドの違い。デバッグビルドでログや assertion が有効になっているなど。
  • 特定のライブラリの有無など、そもそもコンパイルできないケース。

これらは #ifdef で無効になるコードが出てくるのは避けられないですね。それでも、違いを吸収するためのレイヤを設ける(簡単にはマクロの定義内容を変えるなど)といったことで、#ifdef を意識しなければならない範囲を最小限に留めるべきです。あちこちに #ifdef がばらまかれてしまうの避けたほうがよいでしょう。

設計的な観点

#ifdef は飛び道具です。ときに強力な武器となりますが、基本的には飛び道具に頼るのではなく、ふるまいの違いは設計に反映するべきです。オブジェクト指向であればクラスの違いとして表現し、Strategy Pattern で使い分ける、といった方法が定番でしょう。

また、設計の高尚な話じゃなくても #ifdef だらけのコードは読みにくく、メンテナンスもしにくいです。#ifdef が必要になるにしても、切り替える場所はできるだけ最小限 (1,2 ヶ所にできることが多い) で済むように工夫したほうがよいでしょう。

まとめ

  • #ifdef (#if) は CI 環境によってビルド・テストされ続ける恩恵を受けにくくします。
    • できるだけたくさんのコードがビルド・テストされるように工夫しましょう
  • #ifdef (#if) は飛び道具。基本的には頼らずにふつうの設計で表現しましょう。