英語版
このページの英語版を見る

Cプリプロセッサ vs D

C言語が開発された当時、コンパイラ技術は原始的なものであった。 テキストマクロプリプロセッサを フロントエンドにインストールすることは、多くの強力な機能を追加する わかりやすく簡単な方法であった。 プログラムのサイズと複雑性の増大により、 これらの機能には多くの固有の問題があることが明らかになった。 D言語にはプリプロセッサはないが、 D言語は同じ問題を解決するよりスケーラブルな手段を提供する。


ヘッダーファイル

Cプリプロセッサの方法

CおよびC++は、ヘッダーファイルのテキスト形式のインクルードに大きく依存している。 このため、コンパイラはソースファイルごとに何万行ものコードを何度も何度も再コンパイルしなければならないことがよくあり、 これは明らかにコンパイル時間を遅くする原因となる。通常、ヘッダーファイルが使用されるのは、 テキスト形式ではなくシンボリック形式で挿入する方がより適切である場合である。 これは、インポート文を使用して行う。シンボリック形式のインクルードとは、コンパイラが すでにコンパイル済みのシンボルテーブルをロードするだけです。複数のインクルードを防ぐマクロ「ラッパー」や、 #pragma onceの複雑な構文、 プリコンパイル済みヘッダーの理解しがたい脆弱な構文は、 Dにとっては単に不要で無関係なものです。

#include <stdio.h>

D Wayでは

Dはシンボリックインポートを使用する:

import core.stdc.stdio;

#pragma once

Cプリプロセッサの方法

Cヘッダーファイルは、#includeが複数回行われることを防ぐ必要がある場合がよくある。 これを実現するには、ヘッダーファイルに次の行を含める。

#pragma once

または、より移植性の高いもの:

#ifndef __STDIO_INCLUDE
#define __STDIO_INCLUDE
... header file contents
#endif

Dの方法

Dはインポートファイルをシンボリックにインクルードするので、これは全く不要である。 インポートは、インポート宣言が何度現れても、一度だけ行われる。


#pragma pack

Cプリプロセッサの方法

これはC言語で"構造体"のアラインメントを調整するために使用される。

Dの方法

Dクラスでは、アラインメントを調整する必要はない(実際、 コンパイラは最適なレイアウトを得るためにデータフィールドを自由に再配置できる。 コンパイラがスタックフレーム上のローカル変数を再配置するのと同じである)。 D構造体が外部で定義されたデータ構造にマッピングされる場合は、 調整が必要であり、次のように処理される。

struct Foo
{
  align (4):  // 4バイトアライメントを使用する
    ...
}

マクロ

プリプロセッサマクロは、C言語に強力な機能と柔軟性を追加する。しかし、 それには欠点もある。

以下に、マクロの一般的な用途と、 D言語における対応する機能を示す。

  1. リテラル定数の定義:

    Cプリプロセッサの方法

    #define VALUE   5
    

    Dの方法

    DD
    enum int VALUE = 5;
    
  2. 値またはフラグのリストを作成する:

    Cプリプロセッサの方法

    int flags:
    #define FLAG_X  0x1
    #define FLAG_Y  0x2
    #define FLAG_Z  0x4
    ...
    flags |= FLAG_X;
    

    Dの方法

    wayway
    enum FLAGS { X = 0x1, Y = 0x2, Z = 0x4 };
    FLAGS flags;
    ...
    flags |= FLAGS.X;
    
  3. ASCII文字とwchar文字の区別:

    Cプリプロセッサの方法

    #if UNICODE
        #define dchar       wchar_t
        #define TEXT(s)     L##s
    #else
        #define dchar       char
        #define TEXT(s)     s
    #endif
    
    ...
    dchar h[] = TEXT("hello");
    

    Dの方法

    way方法
    dstring h = "hello";
    

    Dのオプティマイザは関数をインライン化し、 文字列定数の変換をコンパイル時に実行する。

  4. レガシーコンパイラのサポート:

    Cプリプロセッサの方法

    #if PROTOTYPES
    #define P(p)    p
    #else
    #define P(p)    ()
    #endif
    int func P((int x, int y));
    

    Dの方法

    Dコンパイラをオープンソースにすることで、 構文の逆互換性の問題を大幅に回避できる。
  5. 型エイリアシング:

    Cプリプロセッサの方法

    #define INT     int
    

    Dの方法

    wayway
    alias INT = int;
    
  6. 宣言と定義の両方に1つのヘッダーファイルを使用する方法:

    Cプリプロセッサの方法

    #define EXTERN extern
    #include "declarations.h"
    #undef EXTERN
    #define EXTERN
    #include "declarations.h"
    
    declarations.h ファイルで:
    EXTERN int foo;
    

    Dの方法

    宣言と定義は同じなので、 同じソースから宣言と定義の両方を生成するために記憶クラスを変更する必要はない 。
  7. 軽量インライン関数:

    Cプリプロセッサの方法

    #define X(i)    ((i) = (i) / 3)
    

    Dの方法

    int X(ref int i) { return i = i / 3; }
    
    コンパイラの最適化機能によりインライン化されるため、効率は失われない。
  8. 関数ファイルと行番号情報をアサートする:

    Cプリプロセッサの方法

    #define assert(e)       ((e) || _assert(__LINE__, __FILE__))
    

    Dの方法

    assert() は組み込み式の原始関数である。 コンパイラにこのような assert() の知識を与えることで、 _assert() 関数が決して戻らないことなどについても
  9. 関数呼び出し規約の設定:

    Cプリプロセッサの方法

    #ifndef _CRTAPI1
    #define _CRTAPI1 __cdecl
    #endif
    #ifndef _CRTAPI2
    #define _CRTAPI2 __cdecl
    #endif
    
    int _CRTAPI2 func();
    

    Dの方法

    呼び出し規約はブロックで指定できるため、 すべての関数に対して変更する必要はない。
    extern (Windows)
    {
        int onefunc();
        int anotherfunc();
    }
    
  10. __nearまたは__farポインタの奇妙な挙動を隠蔽する:

    Cプリプロセッサの方法

    #define LPSTR   char FAR *
    

    Dの方法

    Dは16ビットコード、異なるポインタサイズの混在、異なる種類のポインタをサポートしていないため、 この問題は単に 無関係である。
  11. シンプルな汎用プログラミング:

    Cプリプロセッサの方法

    テキスト置換に基づいて使用する関数を選択する:
    #ifdef UNICODE
    int getValueW(wchar_t *p);
    #define getValue getValueW
    #else
    int getValueA(char *p);
    #define getValue getValueA
    #endif
    

    Dの方法

    Dは、他のシンボルのエイリアスであるシンボルの宣言を可能にする。
    version (UNICODE)
    {
        int getValueW(wchar[] p);
        alias getValue = getValueW;
    }
    else
    {
        int getValueA(char[] p);
        alias getValue = getValueA;
    }
    

条件付きコンパイル

Cプリプロセッサの方法

条件付きコンパイルはCプリプロセッサの強力な機能であるが、 欠点もある。

D言語の方法

Dは条件付きコンパイルをサポートしている。

  1. バージョン固有の関数を個別のモジュールに分離する。
  2. デバッグハーネスを有効化/無効化するデバッグ文、 余分な印刷など
  3. 単一のソースセットから生成されたプログラムの複数のバージョンを処理するためのバージョン文 。
  4. if (0) 文。
  5. ネストされたコメント /+ +/ は、コードブロックをコメントアウトするために使用できる。

コードのファクタリング

Cプリプロセッサの方法

関数内で、複数の場所で実行されるコードの反復的なシーケンスが存在することはよくある。 パフォーマンス上の 考慮事項により、それを別の関数として分離することはできないため、マクロとして実装される。例えば、 バイトコードインタプリタの次の断片を考えてみよう。

unsigned char *ip;  // バイトコード命令ポインタ
int *stack;
int spi;            // スタックポインター
...
#define pop()       (stack[--spi])
#define push(i)     (stack[spi++] = (i))
while (1)
{
    switch (*ip++)
    {
        case ADD:
            op1 = pop();
            op2 = pop();
            result = op1 + op2;
            push(result);
            break;

        case SUB:
        ...
    }
}

これは数多くの問題を抱えている。

  1. マクロは式として評価されなければならず、変数を宣言することはできない。 スタックのオーバーフロー/アンダーフローをチェックするために拡張することの難しさを考えてみよう。
  2. マクロは意味シンボルテーブルの外側に存在するため、 宣言された関数の外側でも有効なままである。
  3. マクロのパラメータは値ではなくテキスト形式で渡されるため、 マクロの実装では パラメータを複数回使用しないよう注意する必要があり、()で保護しなければならない。
  4. マクロはデバッガには見えず、デバッガには展開された式のみが表示される。

Dの方法

D はネストした関数を使用して、この問題をうまく解決している。

ubyte* ip;      // バイトコード命令ポインター
int[] stack;    // オペランドスタック
int spi;        // スタックポインタ
...

int pop()        { return stack[--spi]; }
void push(int i) { stack[spi++] = i; }

while (1)
{
    switch (*ip++)
    {
        case ADD:
            op1 = pop();
            op2 = pop();
            push(op1 + op2);
            break;

        case SUB:
        ...
    }
}

問題に対処する。

  1. ネストされた関数には、D関数の表現力をフルに利用できる。 配列アクセスはすでに境界チェック済み (コンパイル時のスイッチで調整可能)。
  2. ネストされた関数名は、他の名前と同様にスコープが設定される。
  3. パラメータは値として渡されるため、 パラメータ式の副作用を心配する必要はない。
  4. ネストした関数はデバッガから参照できる。

さらに、ネストされた関数はインライン化することができ、 C マクロ版と同等の高いパフォーマンスを実現する。


#error と static assert

static assertは、コンパイル時に実行されるユーザー定義のチェックである。 チェックに失敗すると、コンパイル時にエラーが発生し、コンパイルが失敗する。

Cプリプロセッサの方法

最初の方法は、#error プリプロセッシング指令を使用することである。

#if FOO || BAR
    ... code to compile ...
#else
#error "there must be either FOO or BAR"
#endif

これは、プリプロセッサ式に内在する制限がある (すなわち、整数定数式のみ、キャストなし、sizeof なし、 シンボリック定数なしなど)。

これらの問題は、static_assert マクロを定義することで(M. Wilson氏に感謝する)

#define static_assert(_x) do { typedef int ai[(_x) ? 1 : 0]; } while(0)

そして、次のように使用する:

void foo(T t)
{
    static_assert(sizeof(T) < 4);
    ...
}

これは、条件が falseと評価 された場合にコンパイル時の意味エラーを引き起こすことで機能する。このテクニックの限界は、 コンパイラから時折非常にわかりにくいエラーメッセージが出力されることと、 関数本体の外側でstatic_assert を使用できないことである。

D Way

Dには"static assert"という機能があり、 これは宣言や文が使用できる場所であればどこでも使用できる。 例えば:

version (FOO)
{
    class Bar
    {
        const int x = 5;
        static assert(Bar.x == 5 || Bar.x == 6);

        void foo(T t)
        {
            static assert(T.sizeof < 4);
            ...
        }
    }
}
else version (BAR)
{
    ...
}
else
{
    static assert(0);   // サポートされていないバージョン
}

テンプレートミックスイン

Dテンプレートミックスインは 表面的には、 Cのプリプロセッサを使用してコードブロックを挿入し、 インスタンス化されたスコープでそれらを解析するのと 同じように見える。 しかし、マクロに対するミックスインの利点は以下の通りである。

  1. ミックスインは、言語の構文に適合する解析済みの宣言ツリーを代用する。 一方、マクロは、 組織化されていない任意のプリプロセッサトークンを代用する。
  2. ミックスインは同じ言語である。マクロは別の独立した言語であり、 C++の上にレイヤー化されており、独自の式規則、 独自の型、独自のシンボルテーブル、独自のスコープ規則などを備えている。
  3. ミックスインは部分的な特殊化ルールに基づいて選択され、マクロには オーバーロードがない。
  4. ミックスインはスコープを作成するが、マクロは作成しない。
  5. ミックスインは構文解析ツールと互換性があるが、マクロは互換性がない。
  6. ミックスインのセマンティック情報およびシンボルテーブルはデバッガーに渡されるが、 マクロは翻訳の過程で失われる。
  7. ミックスインにはオーバーライドの競合を解決するルールがあるが、マクロには 衝突するだけだ。
  8. ミックスインは、必要に応じて標準アルゴリズムを使用して一意の識別子を自動的に作成するが、 マクロでは、苦肉の策としてトークンを貼り付けるなどして手動で作成しなければならない。
  9. 副作用のあるミックスインの値引数は一度だけ評価され、マクロの 値引数は展開で使用されるたびに評価される (奇妙なバグにつながる)。
  10. ミックスイン引数の置換は、演算子の優先順位の再編成を避けるために括弧で囲む必要はない。
  11. ミックスインは通常のDコードとして任意の長さで型指定でき、複数行 マクロはバックスラッシュで改行する必要があり、行末のコメントには「//」を使用できない などである。
  12. ミックスインは他のミックスインを定義できる。マクロは他のマクロを作成できない。