CプログラマのためのD言語
Et tu, D? Then fall, C! -- William Nerdspeare
熟練したCプログラマは誰でも、自然とさまざまなイディオムやテクニックを 身につけているものです。新しい言語を学ぼうとすると、時にこれらの イディオムに慣れすぎたせいで、同じことを別の言語でどう実現するのか わからなくなってしまいます。そこでここに、Cのよく知られたテクニックと、 対応するDでのやり方を集めてみました。
Cにはオブジェクト指向関連の機能はありませんので、 オブジェクト指向の話は C++プログラマのためのD言語 に分けて書きました。
Cのプリプロセッサについては Cプリプロセッサ vs D で扱っています。
- 型のサイズを取得する
- 型の最大・最小値を取得する
- 組み込みデータ型
- 特殊な浮動小数点数の値
- 浮動小数点数の剰余
- 浮動小数点数の比較でのNaNの扱い
- assert
- 配列要素の初期化
- 配列の要素ごとのループ
- 可変長配列の作成
- 文字列連結
- 整形print
- 関数の前方参照
- 引数を持たない関数
- ラベル付きbreakとcontinue
- goto文
- 構造体タグの名前空間
- 文字列の検索
- 構造体メンバのアラインメント
- 無名構造体・無名共用体
- 構造体の型と変数の宣言
- 構造体メンバのオフセット計算
- 共用体の初期化
- 構造体の初期化
- 配列の初期化
- エスケープされた文字列リテラル
- ASCII文字とワイド文字
- 列挙型と配列の併用
- typedefで新しい型を作る
- 構造体の比較
- 文字列の比較
- 配列のソート
- volatile なメモリアクセス
- 文字列リテラル
- データ構造のなぞり
- 符号なし右シフト
- 動的クロージャ
- 可変個関数引数
型のサイズを取得する
C では
sizeof(int) sizeof(char *) sizeof(double) sizeof(struct Foo)
D では
sizeプロパティを使います:
int.sizeof (char *).sizeof double.sizeof Foo.sizeof
型の最大・最小値を取得する
C では
#include <limits.h> #include <math.h> CHAR_MAX CHAR_MIN ULONG_MAX DBL_MIN
D では
char.max char.min ulong.max double.min
組み込みデータ型
C => D
bool => bit char => char signed char => byte unsigned char => ubyte short => short unsigned short => ushort wchar_t => wchar int => int unsigned => uint long => int unsigned long => uint long long => long unsigned long long => ulong float => float double => double long double => real _Imaginary long double => ireal _Complex long double => creal
charは8bitの符号なし、wcharは16bitの符号なし型で、 別々の型です。 これによって型安全性やオーバーロードがしやすくなっています。
Cではintやunsingedのサイズは環境によって変わりますが、Dでは固定されています。
特殊な浮動小数点数の値
C では
#include <fp.h> NAN INFINITY #include <float.h> DBL_DIG DBL_EPSILON DBL_MANT_DIG DBL_MAX_10_EXP DBL_MAX_EXP DBL_MIN_10_EXP DBL_MIN_EXP
D では
double.nan double.infinity double.dig double.epsilon double.mant_dig double.max_10_exp double.max_exp double.min_10_exp double.min_exp
浮動小数点数の剰余
C では
#include <math.h> float f = fmodf(x,y); double d = fmod(x,y); long double r = fmodl(x,y);
D では
Dは余りを取る演算('%') は浮動小数点数に対しても定義されています:float f = x % y; double d = x % y; real r = x % y;
浮動小数点数でのNaNの扱い
C では
Cでは、NaNを比較したときに何が起きるかは規定されていません。 また実際、ほとんどのコンパイラはNaNチェックを行いません。 (Digital Mars C コンパイラはこのチェックを行う数少ない例外です)#include <math.h> if (isnan(x) || isnan(y)) result = FALSE; else result = (x < y);
D では
Dでは、 NaNを考慮した完全な浮動小数比較演算が提供されています。result = (x < y); // x か y が NaN なら false
assertは守りの堅いコードに必須の要素
C では
Cはassertに直接対応はしていませんが __LINE__ を使って assertマクロが定義されています。__FILE__ や __LINE__ マクロの使い道はこれくらいしか無いように思われます。
#include <assert.h> assert(e == 0);
D では
D は assert が言語に組み込まれています。assert(e == 0);
配列要素の初期化
C では
#define ARRAY_LENGTH 17 int array[ARRAY_LENGTH]; for (i = 0; i < ARRAY_LENGTH; i++) array[i] = value;
D では
int array[17];
array[] = value;
配列の要素ごとのループ
C では
配列の要素数は別に定義されているか、sizeof() を使ったややこしいトリックが必要です。
#define ARRAY_LENGTH 17 int array[ARRAY_LENGTH]; for (i = 0; i < ARRAY_LENGTH; i++) func(array[i]);あるいは:
int array[17]; for (i = 0; i < sizeof(array) / sizeof(array[0]); i++) func(array[i]);
D では
Dでは配列のプロパティ"length"から配列サイズが得られます。int array[17]; for (i = 0; i < array.length; i++) func(array[i]);あるいはもっと簡単に:
int array[17]; foreach (int value; array) func(value);
可変長配列の作成
C では
Cでは配列ではこれは不可能です。 サイズを保持する別の変数を作って、 明示的に配列の長さの管理を行う必要があります:#include <stdlib.h> int array_length; int *array; int *newarray; newarray = (int *) realloc(array, (array_length + 1) * sizeof(int)); if (!newarray) error("out of memory"); array = newarray; array[array_length++] = x;
D では
Dには、簡単にリサイズできる動的配列があります。 通常必要なメモリ管理機構は全てD言語側でサポートされています。int[] array;
array.length = array.length + 1;
array[array.length - 1] = x;
文字列連結
C では
いくつか難点がありました。 例えば、メモリの割り当て・解放や、 NULLポインタの扱い、文字列の長さの処理などです。#include <string.h> char *s1; char *s2; char *s; // s1 と s2 をつなげて、結果を s に入れる free(s); s = (char *)malloc((s1 ? strlen(s1) : 0) + (s2 ? strlen(s2) : 0) + 1); if (!s) error("out of memory"); if (s1) strcpy(s, s1); else *s = 0; if (s2) strcpy(s + strlen(s), s2); // s に "hello" に追加 char hello[] = "hello"; char *news; size_t lens = s ? strlen(s) : 0; news = (char *) realloc(s, (lens + sizeof(hello) + 1) * sizeof(char)); if (!news) error("out of memory"); s = news; memcpy(s + lens, hello, sizeof(hello));
D では
Dは~と~=演算子をcharやwcharの配列に対してオーバーロードし、 文字列結合の意味として使用しています。次のように:char[] s1; char[] s2; char[] s; s = s1 ~ s2; s ~= "hello";
整形print
C では
printf() が汎用の整形出力ルーチンです:#include <stdio.h> printf("Calling all cars %d times!\n", ntimes);
D では
D ではどうなっているのでしょう? やはり printf() が使えます:printf("Calling all cars %d times!\n", ntimes);
さらに、writefln() は printf() を型安全かつスレッドセーフに改善します:
import std.stdio; writefln("Calling all cars %s times!", ntimes);
関数の前方参照
C では
関数の前方参照、 つまりまだソースファイル中に現れていない関数の呼び出し、 は出来ません。呼び出しより前に関数のプロトタイプ宣言が必要です。void forwardfunc(); void myfunc() { forwardfunc(); } void forwardfunc() { ... }
D では
プログラム全体がまとめて扱われるので、 前方宣言のコードは不要です。 前方宣言自体、言語として不正とされています! 宣言と定義を分けて二箇所に書くことで生じる面倒さやエラーをDは回避しています。 関数は好きな順序で定義しましょう。void myfunc() { forwardfunc(); } void forwardfunc() { ... }
引数を持たない関数
C では
void function(void);
D では
D は型付けの強い言語なので、 引数を持たないことを明示的に宣言する 必要はありません。単に引数宣言を書かなければよいだけです。void function() { ... }
ラベル付きbreakとcontinue
C では
breakとcontinueは、再内周ループにのみ適用されました。 このため、多段breakするにはgotoが使われます:for (i = 0; i < 10; i++) { for (j = 0; j < 10; j++) { if (j == 3) goto Louter; if (j == 4) goto L2; } L2: ; } Louter: ;
D では
break と continue にはラベルを指定できます。 ラベルは文を含むループにつき、breakやcontinueはそのラベルの付いた ループ(あるいはswitch)に対して適用されます。Louter: for (i = 0; i < 10; i++) { for (j = 0; j < 10; j++) { if (j == 3) break Louter; if (j == 4) continue Louter; } } // break Louter ではここに来る
goto文
C では
プロのCプログラマにとっては、 諸悪の根元たるgoto文であっても重要です。 時にはおかしなコントロールフローが必要になることもあります。D では
Dのラベル付きbreak, continue機能によって、 Cでgotoを使っていた場面のほとんどで gotoは不要になります。 しかしDは、全ての規則に例外がある、 ということをよくわかっている現実のプログラマのための現実的な言語です。 もちろんDはgotoに対応しています!Struct tag name space
C では
構造体を使う場所ごとにいちいちstructと書いていくのは大変なので、 次のイディオムが広く使われています:typedef struct ABC { ... } ABC;
D では
構造体のタグ名は別の名前空間には入りません。他の名前と同様に参照できます。 つまり:struct ABC { ... }
文字列の検索
C では
文字列を受け取って、可能な文字列のリストと比較して 対応するアクションを実行する…というコードを考えます。 よくある例として、コマンドライン引数の処理があります:#include <string.h> void dostring(char *s) { enum Strings { Hello, Goodbye, Maybe, Max }; static char *table[] = { "hello", "goodbye", "maybe" }; int i; for (i = 0; i < Max; i++) { if (strcmp(s, table[i]) == 0) break; } switch (i) { case Hello: ... case Goodbye: ... case Maybe: ... default: ... } }3つのデータ構造、構造体と列挙型と表、に加えてswitch文を 同時に管理しなくてはいけない、というのは問題です。 もしデータにもっと多くの可能性があったなら、 三つの構造をきちんと整合性を持たせるのは難しく、 バグの原因になります。 さらに、値の種類が莫大になってくると、単なる線形探索ではなく ハッシュや二分木を使って速度の改善を図る必要が出てきます。 しかし、そのような新しいコードを書いてデバッグする… という手間は時間がかかるため、結局放置される、 というのがよくあるパターンです。
D では
Dではswitch文を、 整数だけでなく文字列に対しても使えるように拡張しました。 文字列照合のコードは次のように直接的になります:void dostring(char[] s) { switch (s) { case "hello": ... case "goodbye": ... case "maybe": ... default: ... } }新しい文字列を追加するのも簡単です。効果的な探索コードの生成は、 コンパイラに任せることが出来ます。手書きするのに比べてバグも減り、 開発時間も短縮されます。
構造体メンバのアラインメント
C では
コマンドラインスイッチによって制御されます。しかし、 全てのモジュールやライブラリを確実に再コンパイルしないとおかしな結果になります。 このため、実際には #pragma が使われます:#pragma pack(1) struct ABC { ... }; #pragma pack()しかし、規格上も実際の実装を見ても、 #pragma は移植性がありません。
D では
Dでは、すべてのDコンパイラ間で共通の、アラインメントを指定する構文が用意されています。 実際に行われるアラインメントは、ABI互換性のために、 各環境でのCコンパイラと合わせて実装されています。 アーキテクチャを越えて同じ特定のメモリレイアウトに合わせるには、 align(1) を使って手動でレイアウトを調整します。
struct ABC { int z; // z はデフォルトの整列 align (1) int x; // x はバイト境界へ align (4) { ... // {} 内の宣言はdword境界へ整列 } align (2): // ここから下ではword境界へ整列 int y; // y はword境界へ }
無名構造体・無名共用体
時に、構造体や共用体をネストして、レイアウトを調整すると便利なことがあります。C では
Cでは無名の構造体,共用体は禁じられています。そこで、 ダミーのタグ名やメンバが必要になります。struct Foo { int i; union Bar { struct Abc { int x; long y; } _abc; char *p; } _bar; }; #define x _bar._abc.x #define y _bar._abc.y #define p _bar.p struct Foo f; f.i; f.x; f.y; f.p;不格好なだけでなく、マクロを使ってしまうとデバッガで扱いにくくなってしまいます。 また、マクロは構造体の中だけでなくグローバルスコープ全域で定義されます。
D では
より自然に 無名構造体,共用体を扱えます。struct Foo { int i; union { struct { int x; long y; } char* p; } } Foo f; f.i; f.x; f.y; f.p;
構造体の型と変数の宣言.
C では
セミコロンで終わる一つの文で書くか、
struct Foo { int x; int y; } foo;
二行に分けで書きます:
struct Foo { int x; int y; }; // note terminating ; struct Foo foo;
D では
構造体の定義と変数宣言は同じ文には書けません:
struct Foo { int x; int y; } // Dでは ; がありません Foo foo;
構造体定義の最後に ; がつかないのは、 関数やブロック {} の最後には ; が不要なのに構造体宣言には要る、という混乱する仕様を排除するためです。
構造体メンバのオフセット計算
C では
通常はマクロを使います:#include <stddef> struct Foo { int x; int y; }; off = offsetof(Foo, y);
D では
offsetというのはプロパティの一つです:struct Foo { int x; int y; } off = Foo.y.offsetof;
共用体の初期化
C では
"最初のメンバ"規則に従って初期化されます:union U { int a; long b; }; union U x = { 5 }; // メンバ 'a' が 5 に初期化される共用体のメンバを増やしたり宣言の順番を変えたりすると、 初期化のところで大変な問題が生じ得ます。
D では
どのメンバを初期化するのか明示的に言及します:union U { int a; long b; } U x = { a:5 };わかりにくさや保守性の問題を回避しています。
構造体の初期化
C では
メンバは、{} の中の順番に応じて初期化されます。struct S { int a; int b; }; struct S x = { 5, 3 };小さな構造体では大した問題ではありませんが、メンバの数が増えてくると、 初期化子を正しく宣言の順に並べる作業に細心の注意が必要となってきます。 そこでメンバが追加されたり並び替えられたりすると、 初期化部分を探して間違いのないよう修正することになります。 もうこれはバグの地雷原とでもいうしかありません。
D では
メンバの明示的な初期化も可能です:struct S { int a; int b; } S x = { b:3, a:5 };意味が明白ですし、順番への依存もありません。
配列の初期化
C では
Cでは順番に依存して配列が初期化されます:int a[3] = { 3,2,2 };ネストした配列の場合は{}があってもなくてもOkです:
int b[3][2] = { 2,3, {6,5}, 3,4 };
D では
配列についてはDも、順番に依存した初期化が出来ます。しかし、 インデックスを明示的に指定することも可能です。以下は皆同じ結果になります。int[3] a = [ 3, 2, 0 ]; int[3] a = [ 3, 2 ]; // C同様、指定されていない部分は0初期化 int[3] a = [ 2:0, 0:3, 1:2 ]; int[3] a = [ 2:0, 0:3, 2 ]; // 指定がなければ、 // インデックスは直前+1配列の添え字をenumで扱っているとき、これは便利です。 enumの順序が変更されたり新しく列挙定数が増えても大丈夫:
enum color { black, red, green } int[3] c = [ black:3, green:2, red:5 ];多重配列の初期化は明示的に [] が必要です:
int[2][3] b = [ [2,3], [6,5], [3,4] ]; int[2][3] b = [[2,6,3],[3,5,4]]; // error
エスケープされた文字列リテラル
C では
Cでは\がエスケープシーケンスなので、DOSファイルシステムのファイル名を表現する時に問題が発生します。c:\root\file.c と指定するには:char file[] = "c:\\root\\file.c";正規表現を扱い出すと、もっと大変になります。 例えば次のような、引用文字列にマッチする正規表現を考えてみましょう:
/"[^\\]*(\\.[^\\]*)*"/
C では、これは笑い話になりかねない表現になります:
char quoteString[] = "\"[^\\\\]*(\\\\.[^\\\\]*)*\"";
D では
文字列は、WYSIWYG (what you see is what you get) です。 エスケープは文字列と分けて書きます。なので:char[] file = `c:\root\file.c`; char[] quoteString = \" r"[^\\]*(\\.[^\\]*)*" \";かの有名な hello world 文字列はこうなるでしょう:
char[] hello = "hello world" \n;
ASCII文字とワイド文字
モダンなプログラムでは、国際化のためにwchar文字を簡単に扱える必要があります。
C では
Cでは、wchar_t型と文字列への接頭辞Lを使います。#include <wchar.h> char foo_ascii[] = "hello"; wchar_t foo_wchar[] = L"hello";ASCIIとワイド文字双方向けにコンパイルできるコードを書こうとすると、 いささか嫌な状況になります。ASCIIとワイド文字の切り替えにマクロが使われるのです:
#include <tchar.h> tchar string[] = TEXT("hello");
D では
文字列の型は意味解析時に決定されるので、マクロ呼び出しで囲う必要はありません:char[] foo_ascii = "hello"; // 文字列はASCIIとされる wchar[] foo_wchar = "hello"; // 文字列はワイド文字とされる
列挙型と配列の併用
C では
次の例を考えます:enum COLORS { red, blue, green, max }; char *cstring[max] = {"red", "blue", "green" };要素の数が少なければ、これで十分です。しかし数を増やすと、正しく管理するのが難しくなってきます。
D では
enum COLORS { red, blue, green } char[][COLORS.max + 1] cstring = [ COLORS.red : "red", COLORS.blue : "blue", COLORS.green : "green", ];完璧ではありませんが、改善されています。
typedefで新しい型を作る
C では
Cのtypedefは'弱い'typedefです。どういうことかというと、 このtypedefは実際には新しい型を定義しない、ということです。 コンパイラはtypedefされた型と元の型を区別しません。typedef void *Handle; void foo(void *); void bar(Handle); Handle h; foo(h); // みつかりにくいバグ bar(h); // okCでの解決策は、型チェックとオーバーロードを提供するだけのために ダミーの構造体型を作ることです。
struct Handle__ { void *value; } typedef struct Handle__ *Handle; void foo(void *); void bar(Handle); Handle h; foo(h); // 構文エラー bar(h); // ok型のデフォルトの初期値を指定するには、マクロ定義が必要です。 命名規則を定めて、その規則に厳密に従わないといけません:
#define HANDLE_INIT ((Handle)-1) Handle h = HANDLE_INIT; h = func(); if (h != HANDLE_INIT) ...構造体を使った方法だと、もっと複雑です:
struct Handle__ HANDLE_INIT; void init_handle() // スタートアップ時にこの関数を呼ぶこと { HANDLE_INIT.value = (void *)-1; } Handle h = HANDLE_INIT; h = func(); if (memcmp(&h,&HANDLE_INIT,sizeof(Handle)) != 0) ...4つも名前を覚えるはめになりました: Handle, HANDLE_INIT, struct Handle__, value.
D では
上のようなイディオムは不要で、単にこう書けます:typedef void* Handle; void foo(void*); void bar(Handle); Handle h; foo(h); bar(h);デフォルト値を扱うには、typedefに初期化子を追加し、 .init プロパティで参照します:
typedef void* Handle = cast(void*)(-1); Handle h; h = func(); if (h != Handle.init) ...覚える名前は一つ、Handle だけで済みます。
構造体の比較
C では
C++では、構造体の代入には単純で便利な方法がありますが:struct A x, y; ... x = y;構造体の比較はそうでもありません。 二つのインスタンスの等値性を比較するには:
#include <string.h> struct A x, y; ... if (memcmp(&x, &y, sizeof(struct A)) == 0) ...この方法は回りくどいですし、 そもそも型チェックによる言語の助けを受けることが全く出来ていません。
しかも、memcmp() による operator==() の実装には困ったバグが潜んでいます。 アラインメントのせいで、構造体のメンバの間には '穴' があるかもしれません。 C はこの穴にどんな値が入るかは保証してくれませんので、 例え全てのメンバの値が等しくても、インスタンスによって'穴'の値が違うせいで "異なっている"と判定されてしまうかもしれません。
D では
Dでは直接的で明確な書き方ができます:A x, y;
...
if (x == y)
...
文字列の比較
C では
ライブラリ関数 strcmp() を使います:char string[] = "hello"; if (strcmp(string, "betty") == 0) // 文字列は一致するだろうか? ...Cは0終端文字列を使うので、 0チェックをする手間の分非効率的です。
D では
== 演算子はいかかでしょう?char[] string = "hello"; if (string == "betty") ...Dの文字列は、 文字列自体と別に長さ情報を保持しています。 従って、Cよりも高速な文字列比較が可能です。(Cでの、 memcmp()とstrcmp()の速度の違いと同じ現象です。)
D は、文字列の比較演算子もサポートしています:
char[] string = "hello"; if (string < "betty") ...ソートや探索に便利です。
配列のソート
C では
バブルソートを何度も繰り返し実装したがるCプログラマも多いようですが、 Cでの正しいソートのしかたは、qsort() を使うことです:int compare(const void *p1, const void *p2) { type *t1 = (type *)p1; type *t2 = (type *)p2; return *t1 - *t2; } type array[10]; ... qsort(array, sizeof(array)/sizeof(array[0]), sizeof(array[0]), compare);全ての型に対して、compare() 関数を書かねばならず、 色々とすぐ打ち間違えをしそうなコードが必要です。
D では
ソートはもっと簡単に書けるべきではないでしょうか:type[] array;
...
array.sort; // 配列を in-place でソート
volatile なメモリアクセス
C では
共有メモリやメモリマップドI/Oなどの volatile なメモリにアクセスするには、volatile へのポインタが使われます:volatile int *p = address; i = *p;
D では
D では、volatile は型修飾子ではなく、文の種類です。int* p = address; volatile { i = *p; }
文字列リテラル
C では
Cの文字列リテラルは複数行で書くことができず、行末\ で各行を繋ぐ必要がありました:"This text spans\n\ multiple\n\ lines\n"長いテキストだった場合、これはかなり退屈な作業です。
D では
次のように、複数行の文字列リテラルが書けます:"This text spans
multiple
lines
"
つまりテキストのブロックは、
単純なコピー&ペーストでDのソースへ取り込めます。
データ構造のなぞり
C では
再帰的な構造のデータをなめる関数を考えます。 ここでは例として、 文字列によるシンボルテーブルとしましょう。 データ構造は二分木の配列です。テーブルを完全に探索して、 特定の文字列がただ一つ含まれているかを 判定する必要があります。このために、木を再帰的にたどる補助関数 membersearchx が要ります。 補助関数には木自体には含まれない、コンテキストの情報が必要です。 そこで、struct Paramblock を定義してパラメータとしてそのポインタを 渡します。
struct Symbol { char *id; struct Symbol *left; struct Symbol *right; }; struct Paramblock { char *id; struct Symbol *sm; }; static void membersearchx(struct Paramblock *p, struct Symbol *s) { while (s) { if (strcmp(p->id,s->id) == 0) { if (p->sm) error("ambiguous member %s\n",p->id); p->sm = s; } if (s->left) membersearchx(p,s->left); s = s->right; } } struct Symbol *symbol_membersearch(Symbol *table[], int tablemax, char *id) { struct Paramblock pb; int i; pb.id = id; pb.sm = NULL; for (i = 0; i < tablemax; i++) { membersearchx(pb, table[i]); } return pb.sm; }
D では
同じアルゴリズムをDで書いたのが下の例で、 劇的にコードが短くなっています。 これはネストした関数が、 その関数を囲んでいるブロックの変数に参照できるため、 Paramblock 構造体やそのポインタが不要となっているからです。 ネストされた補助関数を使うことで、機能の局所性や保守性も向上します。パフォーマンス面でも差はありません。
class Symbol { char[] id; Symbol left; Symbol right; } Symbol symbol_membersearch(Symbol[] table, char[] id) { Symbol sm; void membersearchx(Symbol s) { while (s) { if (id == s.id) { if (sm) error("ambiguous member %s\n", id); sm = s; } if (s.left) membersearchx(s.left); s = s.right; } } for (int i = 0; i < table.length; i++) { membersearchx(table[i]); } return sm; }
符号なし右シフト
C では
右シフト演算子 >> と >>= は、 左オペランドが符号付き整数型ならば符号付き右シフトで、 左オペランドが符号なし整数型ならば符号なし右シフトという動作です。 int変数に対して符号なし右シフトを施すには、 キャストが必要です:int i, j; ... j = (unsigned)i >> 3;確かに i が int 型変数であれば、これはうまく動作します。 しかしもし i がtypedefされた別の型であったなら。
myint i, j; ... j = (unsigned)i >> 3;このときもし myint が実は long int であったら、 unsigned へのキャストは暗黙の内に上位のbitを捨て去り、 結果を破壊してしまうでしょう。
D では
D には C と同様に動作する右シフト演算子 >> と >>= があります。しかし D は、 明示的に符号なし右シフトを行う演算子 >>> と >>>= もまた備えています。 この演算子ならば左オペランドの型を気にする必要はありません。つまり、myint i, j; ... j = i >>> 3;は危険なキャストを避けていて、 しかもどんな整数型に対しても期待したとおりに動作します。
動的クロージャ
C では
再利用可能なコンテナクラスを考えましょう。 再利用可能であるためには、 コンテナ内のそれぞれの要素に任意のコードを適用する操作をサポートすべきです。 これは、関数ポインタを受け取って各要素へ適用する apply 関数を作ることで実現できます。汎用のコンテキスト用ポインタも必要で、 ここでは void *p としてあります。この例はintの配列を使った簡単な例で、 コンテナのユーザがこれらのintの最大値を計算しようとしています。
void apply(void *p, int *array, int dim, void (*fp)(void *, int)) { for (int i = 0; i < dim; i++) fp(p, array[i]); } struct Collection { int array[10]; }; void comp_max(void *p, int i) { int *pmax = (int *)p; if (i > *pmax) *pmax = i; } void func(struct Collection *c) { int max = INT_MIN; apply(&max, c->array, sizeof(c->array)/sizeof(c->array[0]), comp_max); }
この方法で確かに機能を実現はできますが、柔軟性があるとは言い難いものになっています。
D では
D版では、apply 関数へコンテキストの情報を渡すために、 delegate とネスト関数を使います。 どちらも情報の局所性を高めています。class Collection { int[10] array; void apply(void delegate(int) fp) { for (int i = 0; i < array.length; i++) fp(array[i]); } } void func(Collection c) { int max = int.min; void comp_max(int i) { if (i > max) max = i; } c.apply(comp_max); }ポインタや、キャストに汎用ポインタは全てなくなりました。 D版は完全に型安全です。 Dでのもう一つの方法は、関数リテラルを使う方法です:
void func(Collection c) { int max = int.min; c.apply(delegate(int i) { if (i > max) max = i; } ); }特に意味のない関数名を除くことができました。
Variadic Function Parameters
引数の個数が定まらない関数を書く場合を考えます。 例として、 引数全部の和をとる関数があります。C では
#include <stdio.h> #include <stdarg.h> int sum(int dim, ...) { int i; int s = 0; va_list ap; va_start(ap, dim); for (i = 0; i < dim; i++) s += va_arg(ap, int); va_end(ap); return s; } int main() { int i; i = sum(3, 8,7,6); printf("sum = %d\n", i); return 0; }二つほど問題点があります。一つめは、 sum 関数が、何個の引数が渡されたか知らなければならない点です。 Cの場合は個数は明示的に書かれ、 同期を忘れると、 すぐに実際に書かれた引数の個数と合わなくなってしまうこともしばしばあります。 二つめの問題点は、実際に渡された引数がintであって、 doubleでも文字列でも構造体でもない、 ということを保証する術がなにもないことです。
D では
配列型の引数の後ろに ... を続けると、 以降全ての引数を集めて一つの配列を作るようになります。 引数は指定された配列型に対して型検査が行われ、 個数は、 配列のプロパティとして取得することが可能です:import std.stdio; int sum(int[] values ...) { int s = 0; foreach (int x; values) s += x; return s; } int main() { int i; i = sum(8,7,6); writefln("sum = %d", i); return 0; }