条件付きコンパイル

条件付きコンパイルは、特定のコンパイル時条件に応じてプログラムの一部を特別な方法でコンパイルするためのものだ。場合によっては、プログラムのセクション全体を削除してコンパイルしない必要がある場合もある。

条件付きコンパイルでは、コンパイル時に評価可能な条件チェックが行われる。ifforwhileなどの実行時条件文は、条件付きコンパイル機能ではない。

以前の章で既に遭遇したいくつかの機能は、条件付きコンパイルと見なすことができる:

ユニットテストと契約はプログラムの正確性に関するものであり、プログラムに含めるかどうかによってプログラムの動作が変わるべきではない。

Dの条件付きコンパイル専用の機能は以下の通りだ。

is式については、次の章で説明する。

debug

debugは、プログラムの開発中に役立つ。debugとマークされた式および文は、-debugコンパイラスイッチが有効になっている場合にのみプログラムにコンパイルされる。

debug a_conditionally_compiled_expression;

debug {
    // ... 条件付きコンパイルされるコード ...

} else {
    // ... それ以外の場合にコンパイルされるコード ...
}
D

else句はオプションである。

上記の単一の式とコードブロックは、-debugコンパイラスイッチが有効になっている場合にのみコンパイルされる。

プログラムに、"加算"、"減算"などのメッセージを出力する文を追加してきた。このようなメッセージ(別名、ログや ログメッセージ)は、プログラムが実行する手順を視覚化することで、エラーの発見に役立つ。

テンプレート章で説明したbinarySearch()関数を思い出そう。次の関数は、意図的に誤ったものである。

import std.stdio;

// 警告! このアルゴリズムは間違っている
size_t binarySearch(const int[] values, int value) {
    if (values.length == 0) {
        return size_t.max;
    }

    immutable midPoint = values.length / 2;

    if (value == values[midPoint]) {
        return midPoint;

    } else if (value < values[midPoint]) {
        return binarySearch(values[0 .. midPoint], value);

    } else {
        return binarySearch(values[midPoint + 1 .. $], value);
    }
}

void main() {
    auto numbers = [ -100, 0, 1, 2, 7, 10, 42, 365, 1000 ];

    auto index = binarySearch(numbers, 42);
    writeln("Index: ", index);
}
D
cond_comp.1

インデックス42の値は6であるにもかかわらず、プログラムは誤って1と報告している:

インデックス1

プログラム内のバグを特定する一つの方法は、出力にメッセージを表示する行を挿入することだ:

size_t binarySearch(const int[] values, int value) {
    writefln("searching %s among %s", value, values);

    if (values.length == 0) {
        writefln("%s not found", value);
        return size_t.max;
    }

    immutable midPoint = values.length / 2;

    writefln("considering index %s", midPoint);

    if (value == values[midPoint]) {
        writefln("found %s at index %s", value, midPoint);
        return midPoint;

    } else if (value < values[midPoint]) {
        writefln("must be in the first half");
        return binarySearch(values[0 .. midPoint], value);

    } else {
        writefln("must be in the second half");
        return binarySearch(values[midPoint + 1 .. $], value);
    }
}
D

プログラムの出力には、プログラムが実行するステップが含まれるようになった:

[-100, 0, 1, 2, 7, 10, 42, 365, 1000]から42を検索
インデックス4を考慮
後半にある必要がある
42を[10, 42, 365, 1000]から検索
インデックス2を考慮
前半にある必要がある
42を[10, 42] から検索
インデックス1を考慮
インデックス1で42が見つかった
インデックス: 1

前の出力が、プログラマーがバグを見つけるのに実際に役立ったと仮定しよう。バグを見つけて修正したら、writefln()式はもはや必要ないことは明らかだ。しかし、これらの行を削除することは、将来再び役立つ可能性があるため、無駄であるとも考えられる。

これらの行を完全に削除する代わりに、debugとしてマークすることができる:

debug writefln("%s not found", value);
D

これらの行は、-debugコンパイラスイッチが有効になっている場合にのみプログラムに含められる:

dmd deneme.d -ofdeneme -w -debug
Bash
debug(tag)

プログラム内に、おそらくは関連のない部分に、debugキーワードが多数存在する場合、出力結果が過密になる可能性がある。それを回避するために、debug文に名前(タグ)を付けて、プログラムに選択的に含めるようにすることができる。

debug(binarySearch) writefln("%s not found", value);
D

タグが付けられたdebug文は、-debug=tagコンパイラスイッチで有効になる:

dmd deneme.d -ofdeneme -w -debug=binarySearch
Bash

debugブロックにもタグを付けることができる:

debug(binarySearch) {
    // ...
}
D

一度に複数のdebugタグを有効にすることもできる:

dmd deneme.d -w -debug=binarySearch -debug=stackContainer
Bash

その場合、binarySearchstackContainerの両方のデバッグ文およびブロックが組み込まれる。

debug(level)

debug文を数値レベルで関連付ける方が便利な場合もある。レベルを高くすると、より詳細な情報を得ることができる。

debug import std.stdio;

void myFunction(string fileName, int[] values) {
    debug(1) writeln("entered myFunction");

    debug(2) {
        writeln("the arguments:");
        writeln("  file name: ", fileName);

        foreach (i, value; values) {
            writefln("  %4s: %s", i, value);
        }
    }

    // ... 関数の実装 ...
}

void main() {
    myFunction("deneme.txt", [ 10, 4, 100 ]);
}
D
cond_comp.2

指定したレベル以下であるdebug式およびブロックがコンパイルされる。

dmd deneme.d -w -debug=1
./deneme
myFunctionに入った
Bash

次のコンパイルでは、より詳細な情報が提供される:

dmd deneme.d -w -debug=2
./deneme
myFunctionに入った
引数:
  ファイル名: deneme.txt
     0: 10
     1: 4
     2: 100
Bash
version(tag)および version(level)

versiondebugと類似しており、同じように使用される:

version(testRelease) /* ... 式 ... */;

version(schoolRelease) {
    /* ... このプログラムの学校向けに
     *     配布されるバージョンに関連する式 ... */

} else {
    // ... それ以外の場合にコンパイルされるコード ...
}

version(1) aVariable = 5;

version(2) {
    // ... バージョン2の機能 ...
}
D

else句はオプションである。

versionは基本的にdebugと同じように機能するが、別々のキーワードを使用することで、関連のない用途を区別しやすくなる。

debug同様、複数のversionを有効にすることができる:

dmd deneme.d -w -version=record -version=precise_calculation
Bash

versionタグには多くの定義済みタグがあり、その完全なリストは条件付きコンパイルの仕様で確認できる。以下は、その一部を抜粋したリストだ。

事前定義されたversionタグ
コンパイラDigitalMars
GNU
LDC
SDC
オペレーティングシステムWindows
Win32
Win64
linux
OSX
Posix
FreeBSD
OpenBSD
NetBSD
DragonFlyBSD
BSD
Solaris
AIX
Haiku
SkyOS
SysV3
SysV4
Hurd
CPUのエンディアンLittleEndian
BigEndian
有効なコンパイラスイッチD_Coverage
D_Ddoc
D_InlineAsm_X86
D_InlineAsm_X86_64
D_LP64
D_PIC
D_X32
D_HardFloat
D_SoftFloat
D_SIMD
D_Version2
D_NoBoundsChecks
unittest
assert
CPUアーキテクチャX86
X86_64
プラットフォームAndroid
Cygwin
MinGW
ARM
ARM_Thumb
ARM_Soft
ARM_SoftFP
ARM_HardFP
ARM64
PPC
PPC_SoftFP
PPC_HardFP
PPC64
IA64
MIPS
MIPS32
MIPS64
MIPS_O32
MIPS_N32
MIPS_O64
MIPS_N64
MIPS_EABI
MIPS_NoFloat
MIPS_SoftFloat
MIPS_HardFloat
SPARC
SPARC_V8Plus
SPARC_SoftFP
SPARC_HardFP
SPARC64
S390
S390X
HPPA
HPPA64
SH
SH64
Alpha
Alpha_SoftFP
Alpha_HardFP
......

さらに、以下の2つの特別なversionタグがある:

事前定義されたversionタグの使用例として、システム用の改行文字シーケンスを決定するためのstd.asciiモジュールから一部を抜粋して(ここではフォーマットを変更して)以下に示す(static assertについては後で説明する)。

version(Windows) {
    immutable newline = "\r\n";

} else version(Posix) {
    immutable newline = "\n";

} else {
    static assert(0, "Unsupported OS");
}
D
debugversionに識別子を割り当てる

変数と同様に、debugおよびversionには識別子を割り当てることができる。変数とは異なり、この割り当ては値を変更するのではなく、指定した識別子を有効にする。

import std.stdio;

debug(everything) {
    debug = binarySearch;
    debug = stackContainer;
    version = testRelease;
    version = schoolRelease;
}

void main() {
    debug(binarySearch) writeln("binarySearch is active");
    debug(stackContainer) writeln("stackContainer is active");

    version(testRelease) writeln("testRelease is active");
    version(schoolRelease) writeln("schoolRelease is active");
}
D
cond_comp.3

上記のdebug(everything)ブロック内の代入は、指定されたすべての識別子を有効にする:

dmd deneme.d -w -debug=everything
./deneme
binarySearchがアクティブ
stackContainerがアクティブ
testReleaseがアクティブ
schoolReleaseがアクティブ
Bash
static if

static ifは、if文のコンパイル時同等である。

if文と同様に、static ifは論理式を取り、それを評価する。if文とは異なり、static ifは実行フローとは関係なく、コードの一部をプログラムに含めるかどうかを決定する。

論理式は、コンパイル時に評価可能でなければならない。論理式がtrueと評価された場合、static if内のコードがコンパイルされる。条件がfalseの場合、そのコードは、記述されていないかのようにプログラムには含まれない。論理式は、通常、is式および__traitsを利用している。

static ifモジュールスコープまたはstructclass、テンプレートなどの定義内に出現する。オプションで、else句も含まれることがある。

is式を使用して、単純なテンプレートでstatic ifを使ってみよう。static ifの他の例については、次の章で説明する。

import std.stdio;

struct MyType(T) {
    static if (is (T == float)) {
        alias ResultType = double;

    } else static if (is (T == double)) {
        alias ResultType = real;

    } else {
        static assert(false, T.stringof ~ " is not supported");
    }

    ResultType doWork() {
        writefln("The return type for %s is %s.",
                 T.stringof, ResultType.stringof);
        ResultType result;
        // ...
        return result;
    }
}

void main() {
    auto f = MyType!float();
    f.doWork();

    auto d = MyType!double();
    d.doWork();
}
D
cond_comp.4

コードによると、MyTypeは2つの型、floatまたはdoubleでのみ使用できる。doWork()の戻り値の型は、テンプレートがfloatまたはdoubleのいずれでインスタンス化されるかによって選択される。

floatの戻り値の型はdoubleである。
doubleの戻り値の型はrealである。

static if節を連鎖する場合は、else static ifを記述する必要があることに注意。そうしないと、else ifと記述すると、そのif条件式がコードに挿入され、当然、実行時に実行されてしまう。

static assert

これは条件付きコンパイル機能ではないが、static assertをここで紹介することにした。

static assertは、assertとコンパイル時に同等である。条件式がfalseの場合、そのアサーションの失敗によりコンパイルは中止される。

static ifと同様に、static assertはプログラム内の任意のスコープに現れることができる。

上記のプログラムで、static assertの例を見た。そこでは、Tfloatまたはdouble以外の型の場合、コンパイルは中止される。

auto i = MyType!int();
D

static assertに与えられたメッセージと共にコンパイルが中止される:

エラー: static assert  "int はサポートされていない"
Undefined

別の例として、特定のアルゴリズムが、特定のサイズの倍数である型でのみ動作するとする。このような条件は、static assertによってコンパイル時に確保できる。

T myAlgorithm(T)(T value) {
    /* このアルゴリズムでは、型Tのサイズは
     * 4の倍数である必要がある。 */
    static assert((T.sizeof % 4) == 0);

    // ...
}
D

この関数がcharで呼び出された場合、次のエラーメッセージが表示され、コンパイルは中止される。

エラー: static assert  (1LU == 0LU)はfalse
Undefined

このようなテストにより、関数が互換性のない型で動作して、誤った結果が生じるのを防ぐことができる。

static assertは、コンパイル時に評価可能な任意の論理式で使用できる。

型トレイト

__traitsキーワードとstd.traitsモジュールは、コンパイル時に型および式に関する情報を提供する。

コンパイラによって収集される情報の一部は、__traitsによってプログラムで使用可能になる。その構文には、traitsキーワードと、そのキーワードに関連するパラメータが含まれる。

__traits(keyword, parameters)
D

キーワードは、照会される情報を指定する。パラメータは型または式で、その意味はそれぞれのキーワードによって決まる。

__traitsで収集できる情報は、テンプレートで特に有用だ。例えば、isArithmeticキーワードは、特定のテンプレートパラメータTが算術型であるかどうかを判断できる。

static if (__traits(isArithmetic, T)) {
    // ... 算術型 ...

} else {
    // ... 算術型ではない ...
}
D

同様に、std.traitsモジュールは、そのテンプレートを通じてコンパイル時に情報を提供する。例えば、std.traits.isSomeCharは、そのテンプレートパラメータが文字型の場合、trueを返す。

import std.traits;

// ...

    static if (isSomeChar!T) {
        // ... char、wchar、またはdchar ...

    } else {
        // ... 文字型ではない ...
    }
D

詳細については、 __traitsのドキュメントおよび std.traitsのドキュメントを参照。

要約