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言語に強力な機能と柔軟性を追加する。しかし、 それには欠点もある。
- マクロにはスコープの概念がなく、定義された時点からソースの終わりまで有効である。 .hファイルやネストされたコードなど、あらゆるものを切り刻む。 何万行ものマクロ定義を#includeする場合、 意図しないマクロの展開を避けることが問題となる。
- マクロはデバッガーには不明である。シンボリックデータを使用してプログラムをデバッグしようとすると、 マクロの展開についてはデバッガーが把握しているものの、マクロ自体は把握していないため、デバッガーの機能が損なわれる。
- マクロは、以前のマクロの変更によりトークンを任意にやり直すことができるため、ソースコードのトークン化を不可能にする。
- マクロの純粋にテキストベースの性質が、恣意的な一貫性のない使用につながり、 マクロを使用するコードにエラーが発生しやすくなる。(この問題の解決策として、 C++のテンプレートが導入された。)
- マクロは、言語の表現能力の欠点を補うために、今でも使用されている。 例えば、ヘッダーファイルの「ラッパー」などである。
以下に、マクロの一般的な用途と、 D言語における対応する機能を示す。
- リテラル定数の定義:
Cプリプロセッサの方法
#define VALUE 5
Dの方法
DDenum int VALUE = 5;
- 値またはフラグのリストを作成する:
Cプリプロセッサの方法
int flags: #define FLAG_X 0x1 #define FLAG_Y 0x2 #define FLAG_Z 0x4 ... flags |= FLAG_X;
Dの方法
waywayenum FLAGS { X = 0x1, Y = 0x2, Z = 0x4 }; FLAGS flags; ... flags |= FLAGS.X;
- 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のオプティマイザは関数をインライン化し、 文字列定数の変換をコンパイル時に実行する。
- レガシーコンパイラのサポート:
Cプリプロセッサの方法
#if PROTOTYPES #define P(p) p #else #define P(p) () #endif int func P((int x, int y));
Dの方法
Dコンパイラをオープンソースにすることで、 構文の逆互換性の問題を大幅に回避できる。 - 型エイリアシング:
Cプリプロセッサの方法
#define INT int
Dの方法
waywayalias INT = int;
- 宣言と定義の両方に1つのヘッダーファイルを使用する方法:
Cプリプロセッサの方法
#define EXTERN extern #include "declarations.h" #undef EXTERN #define EXTERN #include "declarations.h"
declarations.h ファイルで:EXTERN int foo;
Dの方法
宣言と定義は同じなので、 同じソースから宣言と定義の両方を生成するために記憶クラスを変更する必要はない 。 - 軽量インライン関数:
Cプリプロセッサの方法
#define X(i) ((i) = (i) / 3)
Dの方法
int X(ref int i) { return i = i / 3; }
コンパイラの最適化機能によりインライン化されるため、効率は失われない。 - 関数ファイルと行番号情報をアサートする:
Cプリプロセッサの方法
#define assert(e) ((e) || _assert(__LINE__, __FILE__))
Dの方法
assert() は組み込み式の原始関数である。 コンパイラにこのような assert() の知識を与えることで、 _assert() 関数が決して戻らないことなどについても - 関数呼び出し規約の設定:
Cプリプロセッサの方法
#ifndef _CRTAPI1 #define _CRTAPI1 __cdecl #endif #ifndef _CRTAPI2 #define _CRTAPI2 __cdecl #endif int _CRTAPI2 func();
Dの方法
呼び出し規約はブロックで指定できるため、 すべての関数に対して変更する必要はない。extern (Windows) { int onefunc(); int anotherfunc(); }
- __nearまたは__farポインタの奇妙な挙動を隠蔽する:
Cプリプロセッサの方法
#define LPSTR char FAR *
Dの方法
Dは16ビットコード、異なるポインタサイズの混在、異なる種類のポインタをサポートしていないため、 この問題は単に 無関係である。 - シンプルな汎用プログラミング:
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プリプロセッサの強力な機能であるが、 欠点もある。
- プリプロセッサにはスコープの概念がない。 #if/#endif は 完全に構造化されていない、無秩序なコードと混在することがあり 、状況を把握しにくくする。
- 条件付きコンパイルはマクロをトリガーとするが、マクロは プログラムで使用される識別子と競合する可能性がある。
- #if式はC言語の式とは微妙に異なる方法で評価される。
- プリプロセッサ言語は、C言語とは根本的に概念が異なる。 例えば、空白や行終端文字は、 C言語では意味を持たないが、プリプロセッサでは意味を持つ。
D言語の方法
Dは条件付きコンパイルをサポートしている。
- バージョン固有の関数を個別のモジュールに分離する。
- デバッグハーネスを有効化/無効化するデバッグ文、 余分な印刷など
- 単一のソースセットから生成されたプログラムの複数のバージョンを処理するためのバージョン文 。
- if (0) 文。
- ネストされたコメント /+ +/ は、コードブロックをコメントアウトするために使用できる。
コードのファクタリング
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: ... } }
これは数多くの問題を抱えている。
- マクロは式として評価されなければならず、変数を宣言することはできない。 スタックのオーバーフロー/アンダーフローをチェックするために拡張することの難しさを考えてみよう。
- マクロは意味シンボルテーブルの外側に存在するため、 宣言された関数の外側でも有効なままである。
- マクロのパラメータは値ではなくテキスト形式で渡されるため、 マクロの実装では パラメータを複数回使用しないよう注意する必要があり、()で保護しなければならない。
- マクロはデバッガには見えず、デバッガには展開された式のみが表示される。
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: ... } }
問題に対処する。
- ネストされた関数には、D関数の表現力をフルに利用できる。 配列アクセスはすでに境界チェック済み (コンパイル時のスイッチで調整可能)。
- ネストされた関数名は、他の名前と同様にスコープが設定される。
- パラメータは値として渡されるため、 パラメータ式の副作用を心配する必要はない。
- ネストした関数はデバッガから参照できる。
さらに、ネストされた関数はインライン化することができ、 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のプリプロセッサを使用してコードブロックを挿入し、 インスタンス化されたスコープでそれらを解析するのと 同じように見える。 しかし、マクロに対するミックスインの利点は以下の通りである。
- ミックスインは、言語の構文に適合する解析済みの宣言ツリーを代用する。 一方、マクロは、 組織化されていない任意のプリプロセッサトークンを代用する。
- ミックスインは同じ言語である。マクロは別の独立した言語であり、 C++の上にレイヤー化されており、独自の式規則、 独自の型、独自のシンボルテーブル、独自のスコープ規則などを備えている。
- ミックスインは部分的な特殊化ルールに基づいて選択され、マクロには オーバーロードがない。
- ミックスインはスコープを作成するが、マクロは作成しない。
- ミックスインは構文解析ツールと互換性があるが、マクロは互換性がない。
- ミックスインのセマンティック情報およびシンボルテーブルはデバッガーに渡されるが、 マクロは翻訳の過程で失われる。
- ミックスインにはオーバーライドの競合を解決するルールがあるが、マクロには 衝突するだけだ。
- ミックスインは、必要に応じて標準アルゴリズムを使用して一意の識別子を自動的に作成するが、 マクロでは、苦肉の策としてトークンを貼り付けるなどして手動で作成しなければならない。
- 副作用のあるミックスインの値引数は一度だけ評価され、マクロの 値引数は展開で使用されるたびに評価される (奇妙なバグにつながる)。
- ミックスイン引数の置換は、演算子の優先順位の再編成を避けるために括弧で囲む必要はない。
- ミックスインは通常のDコードとして任意の長さで型指定でき、複数行 マクロはバックスラッシュで改行する必要があり、行末のコメントには「//」を使用できない などである。
- ミックスインは他のミックスインを定義できる。マクロは他のマクロを作成できない。
DEEPL APIにより翻訳、ところどころ修正。
このページの最新版(英語)
このページの原文(英語)
翻訳時のdmdのバージョン: 2.109.1
ドキュメントのdmdのバージョン: 2.109.1
翻訳日付 :
HTML生成日時:
編集者: dokutoku