C++プログラマのためのD言語
熟練したC++プログラマは誰でも、自然とさまざまなイディオムやテクニックを 身につけているものです。新しい言語を学ぼうとすると、時にこれらのイディオムに 慣れすぎたせいで、同じことを別の言語でどう実現するのかわからなくなってしまいます。 そこでここに、C++のよく知られたテクニックと、 対応するDでのやり方を集めてみました。参照: CプログラマのためのD言語
- コンストラクタの定義
- 基底クラスの初期化
- 構造体の比較
- typedef された型
- friend
- 演算子オーバーロード
- 名前空間の using
- RAII (Resource Acquisition Is Initialization)
- プロパティ
- 再帰的テンプレート
- メタテンプレート
- Type Traits
コンストラクタの定義
C++ では
コンストラクタはクラスと同じ名前を持ちます:class Foo
{
Foo(int x);
};
D では
コンストラクタは予約語thisで定義されます:class Foo
{
this(int x) { }
}
これがDでの方法です。
基底クラスの初期化
C++ では
基底クラスのコンストラクタは、基底クラス初期化構文で呼び出します:class A { A() {... } };
class B : A
{
B(int x)
: A() // 基底のコンストラクタ呼び出し
{ ...
}
};
D では
基底クラスのコンストラクタは、super構文で呼び出します:class A { this() { ... } }
class B : A
{
this(int x)
{ ...
super(); // 基底のコンストラクタ呼び出し
...
}
}
C++と比べて優れている点として、基底クラスのコンストラクタ呼び出しを、派生クラスのコンストラクタの中の好きな場所に置ける、という点があります。
また、Dではコンストラクタからまた別のコンストラクタを呼び出せます:
class A
{ int a;
int b;
this() { a = 7; b = foo(); }
this(int x)
{
this();
a = x;
}
}
メンバは、コンストラクタの呼び出しより前に定数値で初期化できます。
これを用いると、上のサンプルは次のように書き換わります:
class A
{ int a = 7;
int b;
this() { b = foo(); }
this(int x)
{
this();
a = x;
}
}
構造体の比較
C++ では
C++では、構造体の代入には単純で便利な方法がありますが:struct A x, y;
...
x = y;
構造体の比較はそうでもありません。
二つのインスタンスの等値性を比較するには:
#include <string.h>
struct A x, y;
inline bool operator==(const A& x, const A& y)
{
return (memcmp(&x, &y, sizeof(struct A)) == 0);
}
...
if (x == y)
...
比較したい全ての構造体毎に、演算子オーバーロードが必要です。
しかも、上の==演算子の定義では型チェックによる
言語の助けを受けることが、全く出来ていません。
C++方式のもう一つの問題は、(x == y) と書いてあるのを見ただけでは、
実際に何が起きるのか何もわからないということです。
実際の挙動を確認するには、適用される operator==()
の定義を探して見る必要があります。
しかも、memcmp() による operator==() の実装には困ったバグが潜んでいます。 アラインメントのせいで、構造体のメンバの間には ‘穴’ があるかもしれません。 C++ はこの穴にどんな値が入るかは保証してくれませんので、 例え全てのメンバの値が等しくても、インスタンスによって'穴'の値が違うせいで "異なっている"と判定されてしまうかもしれません。
これを避けるには結局、operator==() では各メンバを一つ一つ比較していく ことになります。しかし残念なことに、これは信頼性の高い方法ではありません。 なぜなら (1) 構造体にメンバを追加したときに、operator==() への追加を 忘れるかもしれません (2) 浮動小数点数のNaNは、ビットパターンが同じでも 等しくないと判定されます。
結局、C++の範囲ではロバストな解決策はありません。
D では
Dでは直接的で明確な書き方ができます:A x, y;
...
if (x == y)
...
typedef された型
C++ では
C++のtypedefは'弱い'typedefです。どういうことかというと、 このtypedefは実際には新しい型を定義しない、ということです。 コンパイラはtypedefされた型と元の型を区別しません。#define HANDLE_INIT ((Handle)(-1))
typedef void *Handle;
void foo(void *);
void bar(Handle);
Handle h = HANDLE_INIT;
foo(h); // みつかりにくいバグ
bar(h); // ok
C++での解決策は、型チェックとオーバーロードを提供するだけのために
ダミーの構造体型を作ることです。
#define HANDLE_INIT ((void *)(-1))
struct Handle
{ void *ptr;
// デフォルト初期化子
Handle() { ptr = HANDLE_INIT; }
Handle(int i) { ptr = (void *)i; }
// 元の型への変換
operator void*() { return ptr; }
};
void bar(Handle);
Handle h;
bar(h);
h = func();
if (h != HANDLE_INIT)
...
D では
上のようなイディオムは不要で、単にこう書けます:typedef void* Handle = cast(void*)-1;
void bar(Handle);
Handle h;
bar(h);
h = func();
if (h != Handle.init)
...
typedefされた型ごとに元の型と違うデフォルト初期化値をあたえられる点にも、
ご注目下さい。
friend
C++ では
しばしば、二つのクラスが非常に強く結びついていて、 継承関係にはないけれど互いのprivateメンバにアクセスしたい、 ということがあります。これは friend 宣言で実現します:class A
{
private:
int a;
public:
int foo(B *j);
friend class B;
friend int abc(A *);
};
class B
{
private:
int b;
public:
int bar(A *j);
friend class A;
};
int A::foo(B *j) { return j->b; }
int B::bar(A *j) { return j->a; }
int abc(A *p) { return p->a; }
D では
Dでは、同じモジュールのメンバどうしは暗黙のうちにfriend関係になります。 強く結びついたクラスは同じモジュールにあるべき、 というのは理にかなっていますから、 モジュール内はfriendとする、というのは巧みな解決策です:module X;
class A
{
private:
static int a;
public:
int foo(B j) { return j.b; }
}
class B
{
private:
static int b;
public:
int bar(A j) { return j.a; }
}
int abc(A p) { return p.a; }
private 属性は、
他のモジュールからのメンバへのアクセスを禁じます。
演算子オーバーロード
C++ では
struct を使って新しい算術データ型を作ったならば、 intと比較できるように比較演算子をオーバーロードすると便利です:struct A
{
int operator < (int i);
int operator <= (int i);
int operator > (int i);
int operator >= (int i);
};
int operator < (int i, A &a) { return a > i; }
int operator <= (int i, A &a) { return a >= i; }
int operator > (int i, A &a) { return a < i; }
int operator >= (int i, A &a) { return a <= i; }
合わせて8個の関数が必要でした。
D では
Dは、比較演算子どうしには本質的に互いに関係があることを認識しています。 その結果、関数は一つだけしか必要ありません:struct A
{
int opCmp(int i);
}
コンパイラは自動で <, <=, > and >=
を解釈して、
左オペランドがオブジェクトへの参照でない場合も含めて、
opCmp
関数を使って適切に処理します。
同様の賢い規則は他の演算子のオーバーロードにでも成り立ちます。 Dでの演算子オーバーロードはより手間いらずで、エラーの出にくい 設計になっています。C++と同じことを為し遂げるのにより少ないコードで 十分用が足ります。
名前空間の using
C++ では
C++ の using-declaration は、 他の名前空間の名前を現在のスコープへと持ち込みます:namespace foo
{
int x;
}
using foo::x;
D では
Dは名前空間と#includeの代わりにモジュールを使い、 using宣言の代わりにはalias宣言を使用します:/** モジュール foo.d **/
module foo;
int x;
/** 別のモジュール **/
import foo;
alias foo.x x;
alias は using宣言よりも高い柔軟性をそなえています。
シンボルの名前付け替えや、テンプレートメンバの参照、
ネストされたクラス名への名前付けなども alias で可能です。
RAII (Resource Acquisition Is Initialization)
C++ では
C++では、メモリなどのようなリソースは、 全て明示的に扱う必要があります。 スコープを抜けるときには自動的にデスクトラクタが呼び出されるので、 リソースを解放するコードはデストラクタに置くことで、RAIIが実装されます:class File
{ Handle *h;
~File()
{
h->release();
}
};
D では
リソース管理の問題の多くは、 メモリ利用の追跡と解放です。 この問題はDではガベージコレクタによって自動的に処理されています。 二番目によく使われるリソースはセマフォやロックですが、 synchronized 宣言/文 で自動的に処理されます。残った数少ないRAIIの問題は、scope クラスで扱います。 scopeクラスのデストラクタはスコープ終了と同時に呼び出されます。
scope class File
{ Handle h;
~this()
{
h.release();
}
}
void test()
{
if (...)
{ scope f = new File();
...
} // 閉じ括弧に来ると f.~this() が呼ばれます。
// 例え例外が投げられたときでも。
}
プロパティ
C++ では
オブジェクト指向の考え方に沿って、 フィールドを定義するのと一緒に get や set 関数を作る習慣は一般的です:class Abc
{
public:
void setProperty(int newproperty) { property = newproperty; }
int getProperty() { return property; }
private:
int property;
};
Abc a;
a.setProperty(3);
int x = a.getProperty();
これらはタイプ量はちょっとしたものですし、getProperty() や
setProperty() の呼び出しで溢れかえって、
コードを読みにくくする傾向もあります。
D では
プロパティは通常のフィールドアクセスの構文で get/set でき、 しかしgetとsetの際には代わりにメソッドが呼び出されます。class Abc
{
// set
void property(int newproperty) { myprop = newproperty; }
// get
int property() { return myprop; }
private:
int myprop;
}
これは次のように使います:
Abc a;
a.property = 3; // a.property(3) と同じ
int x = a.property; // int x = a.property() と同じ
つまり、
D ではプロパティは単にフィールド名と同じように扱うことができます。
最初は本物のフィールド名として書き始めて後で読み書き用の関数を
かませる必要ができたときも、
クラス定義以外のコードを書き直す必要が全くありません。
派生クラスでオーバーライドの必要ができるかもしれないので
‘万が一のために’ get/setプロパティを定義しておく、
と言った冗長な慣習は不要のもととなります。
"データフィールドを持たないけれども、構文上は持っているかのように動作する
インターフェイスクラス" を作る方法としても活用できます。
再帰的テンプレート
C++ では
テンプレートの発展的な使い方としては、 特殊化によって停止を期待して、再帰的なテンプレート展開を行うことです。 階乗を計算するテンプレートはこうなります:template<int n> class factorial
{
public:
enum { result = n * factorial<n - 1>::result };
};
template<> class factorial<1>
{
public:
enum { result = 1 };
};
void test()
{
printf("%d\n", factorial<4>::result); // prints 24
}
D では
D版も同様ですが、若干シンプルになっています。 テンプレートメンバが一つだけの時は 識別子が周囲の名前空間へ昇格できる、という性質をうまく利用しています:template factorial(int n)
{
enum { factorial = n * .factorial!(n-1) }
}
template factorial(int n : 1)
{
enum { factorial = 1 }
}
void test()
{
writefln("%d", factorial!(4)); // 24を表示
}
メタテンプレート
問題: 最低 nbits のサイズを持つ符号付き整数型への typedef を作りたい。C++ では
この例は、Dr. Carlo Pescio による記事 Template Metaprogramming: Make parameterized integers portable with this novel technique. を簡単にしたものです。C++では、テンプレート引数を使った式の結果に基づく条件コンパイルは、 不可能です。そこで全ての制御フローは、 多数の明示テンプレート特殊化とのパターンマッチングを 追うことで実現されます。 残念なことに、"より小さいか等しい" といった関係に基づくテンプレート特殊化を 記述する方法がないため、今回の例は、 テンプレートの再帰的展開を使った巧妙な技を使用します。 つまり、境界値にマッチするまでテンプレート引数値を1ずつ増やしていくことで、 "より小さいか等しい" を実現しているのです。特殊化版にマッチしなかった場合、 コンパイラの止まらない再帰によるスタックオーバーフローや内部エラーか、 良くても、 奇妙な構文エラーが出力されます。
template typedef の不在を補うために、 プリプロセッサマクロも必要です。
#include <limits.h>
template< int nbits > struct Integer
{
typedef Integer< nbits + 1 > :: int_type int_type ;
} ;
struct Integer< 8 >
{
typedef signed char int_type ;
} ;
struct Integer< 16 >
{
typedef short int_type ;
} ;
struct Integer< 32 >
{
typedef int int_type ;
} ;
struct Integer< 64 >
{
typedef long long int_type ;
} ;
// 要求されたサイズをサポートしていない場合、メタプログラムは、
// 内部エラーが発生するか INT_MAX に達するまで
// カウンタを増やし続けます。 INT_MAX の特殊化版は
// int_type を提供しないようにすることで、
// 常にコンパイルエラーを起こすことができます。
struct Integer< INT_MAX >
{
} ;
// ちょっとした構文糖
#define Integer( nbits ) Integer< nbits > :: int_type
#include <stdio.h>
int main()
{
Integer( 8 ) i ;
Integer( 16 ) j ;
Integer( 29 ) k ;
Integer( 64 ) l ;
printf("%d %d %d %d\n",
sizeof(i), sizeof(j), sizeof(k), sizeof(l));
return 0 ;
}
The C++ Boost Way
これは C++ Boost library を使ったバージョンです。 David Abrahams によって書かれました。#include <boost/mpl/if.hpp>
#include <boost/mpl/assert.hpp>
template <int nbits> struct Integer
: mpl::if_c<(nbits <= 8), signed char
, mpl::if_c<(nbits <= 16), short
, mpl::if_c<(nbits <= 32), long
, long long>::type >::type >
{
BOOST_MPL_ASSERT_RELATION(nbits, <=, 64);
}
#include <stdio.h>
int main()
{
Integer< 8 > i ;
Integer< 16 > j ;
Integer< 29 > k ;
Integer< 64 > l ;
printf("%d %d %d %d\n",
sizeof(i), sizeof(j), sizeof(k), sizeof(l));
return 0 ;
}
D では
D版も再帰テンプレートを使って書いても構いませんが、 もっといい方法があります。 C++の例とは違って、このコードによって何が起きているのかが 極めてわかりやすくなっています。 コンパイル速度も速く、コンパイル失敗するときも、 何が起きたかわかりやすいエラーメッセージを表示します。import std.stdio;
template Integer(int nbits)
{
static if (nbits <= 8)
alias byte Integer;
else static if (nbits <= 16)
alias short Integer;
else static if (nbits <= 32)
alias int Integer;
else static if (nbits <= 64)
alias long Integer;
else
static assert(0);
}
int main()
{
Integer!(8) i ;
Integer!(16) j ;
Integer!(29) k ;
Integer!(64) l ;
writefln("%d %d %d %d",
i.sizeof, j.sizeof, k.sizeof, l.sizeof);
return 0;
}
Type Traits
"Type traits" とは、 コンパイル時に型固有の情報を探し出す方法の別名です。C++ では
以下のテンプレートは、 C++ Templates: The Complete Guide, David Vandevoorde, Nicolai M. Josuttis の353ページから抜粋したものです。 テンプレート引数型が関数であるかどうかを判定しています:template<typename T> class IsFunctionT
{
private:
typedef char One;
typedef struct { char a[2]; } Two;
template<typename U> static One test(...);
template<typename U> static Two test(U (*)[1]);
public:
enum { Yes = sizeof(IsFunctionT<T>::test<T>(0)) == 1 };
};
void test()
{
typedef int (fp)(int);
assert(IsFunctionT<fp>::Yes == 1);
}
このテンプレートは、SFINAE (Substitution Failure Is Not An Error) 原則を利用しています。
そのため、テンプレートの話題としては高度な部類に属するものとされています。
D では
Dでは、 SFINAE (Substitution Failure Is Not An Error) やテンプレート引数のパターンマッチといった小技に頼らず実現できます:template IsFunctionT(T)
{
static if ( is(T[]) )
const int IsFunctionT = 0;
else
const int IsFunctionT = 1;
}
void test()
{
typedef int fp(int);
assert(IsFunctionT!(fp) == 1);
}
型が関数型かどうかを判定するという処理は、テンプレートの力を借りる必要は全くありませんし、
ましてや配列型の不正な配列を作ろうとするなどというおかしなコードなど、
全く不要です。
Dの IsExpression がまさにこの仕事をやってのけます:
void test()
{
alias int fp(int);
assert( is(fp == function) );
}