D 1.0   D 2.0
About Japanese Translation

Last update Tue Nov 27 21:19:29 2007

Dの文字列 vs C++の文字列

何故D言語では、C++の文字列のように全てをライブラリに入れるのではなく、 文字列を言語のコアに組み込んでいるのでしょう?どこがポイントで、どこが改善されているのでしょうか?

結合演算子

C++ の文字列は、既存の演算子のオーバーロードによって成り立っています。 結合に対しては += と + が明白な選択とされました。しかし、 ただコードを見るだけの人は + を見て、"足し算" だと思うでしょう。 その人は型情報を探して(しかも型はしばしば、 何回も typedef した奥の方に埋まっていたりします)文字列型であることを突き止め、 文字列を足しているのではなく結合しているのだ、ということがやっとわかるのです。

さらに言えば、float の配列があるとき、'+' はオーバーロードされて ベクトルの加算の意味になるのでしょうか、それとも配列の結合の意味になるのでしょうか?

Dでは、新しい二項演算子 ~ を結合演算子として導入することで、 この問題を解決しました。これは配列(文字列は配列の一種です) に対しても動作します。~= はこれに応じて、 末尾追加演算になります。 floatの配列の ~ なら結合で、+ はベクトル加算を暗示するでしょう。 新しい演算子を追加することで、配列の扱いを直交的で一貫性のあるものにできます。 (Dでは、文字列は単に文字の配列であって、 特別な型ではありません。)

C言語の文字列構文との相互運用

演算子のオーバーロードは、オペランドの最低どちらか一方が オーバーロード可能な時のみ有効です。このため、C++ の string クラスは文字列を含む任意の式を扱うことができません。次の例を考えて下さい:

const char abc[5] = "world";
string str = "hello" + abc;

これは動作しません。しかし、言語自体が文字列について知っていれば、 動作するのです。

const char[5] abc = "world";
char[] str = "hello" ~ abc;

C言語の文字列構文との一貫性

C++では、文字列の長さを調べるのに三つの方法があります:

const char abc[] = "world";	:	sizeof(abc)/sizeof(abc[0])-1
				:	strlen(abc)
string str;			:	str.length()

この手の一貫性のなさは、汎用のテンプレートを書くときに問題になります。 Dの場合を考えてみて下さい:

char[5] abc = "world";	:	abc.length
char[] str		:	str.length

空文字列チェック

C++ の string では文字列が空かどうかの判定に関数を使います:

string str;
if (str.empty())
	// 文字列は空

Dでは、空文字列の長さはゼロです:

char[] str;
if (!str.length)
	// 文字列は空

既存の文字列のサイズ変更

C++ ではこれをメンバ関数 resize() で扱います:

string str;
str.resize(newsize);

D では str が配列と知っているという利点がありますから、 リサイズは単にlengthプロパティを変更するのみです:

char[] str;
str.length = newsize;

文字列のスライシング

C++ では特別なコンストラクタによって、既存の文字列の一部分を切り出します:

string s1 = "hello world";
string s2(s1, 6, 5);		// s2 は "world"

Dには、C++では不可能な配列切り出しの構文があります:

char[] s1 = "hello world";
char[] s2 = s1[6 .. 11];	// s2 は "world"

これはもちろん、文字列だけでなくDの配列全てに使えます。

文字列の複写

C++ での replace 関数は文字列の複写を行います:

string s1 = "hello world";
string s2 = "goodbye      ";
s2.replace(8, 5, s1, 6, 5);	// s2 は "goodbye world"

D ではスライスを左辺値として使います:

char[] s1 = "hello world";
char[] s2 = "goodbye      ".dup;
s2[8..13] = s1[6..11];		// s2 は "goodbye world"

.dup は、D では文字列リテラルが読み取り専用なために必要となります。 この .dup でコピーを作成することで、 書き込み可能な文字列が作られます。

C文字列への変換

これは C の API との互換性のために必要となります。 C++ では、c_str() メンバ関数を使います:

void foo(const char *);
string s1;
foo(s1.c_str());

Dでは、文字列は.ptrによってchar*へ変換します:

void foo(char*);
char[] s1;
foo(s1.ptr);

これはしかし、foo がゼロ終端文字列を期待している場合、 s1 の末尾に0がついていないと正しく動作しません。代わりに、 std.string.toStringz 関数を使うとそれが保証されます:

void foo(char*);
char[] s1;
foo(std.string.toStringz(s1));

配列の境界チェック

C++ では、[] での文字列の境界チェックは行われません。 Dでは、配列の境界チェックはデフォルトでONで、 デバッグの後にコンパイラのスイッチによってOFFにもできます。

文字列によるswtich文

これはC++では不可能で、 ライブラリに付け加えることも不可能です。 Dでは、明白な構文で記述されます:

switch (str)
{
    case "hello":
    case "world":
	...
}

このとき str は、文字列リテラル"string"や、char[10] のような固定長文字配列、 あるいは char[] のような動的文字列のどれであっても構いません。 高品質な実装では、もちろん、case 文字列の内容によって効率的にこのswitch文を実装する戦略を選ぶことができます。

文字列を埋める

C++では、replace() メンバ関数で実現されています:

string str = "hello";
str.replace(1,2,2,'?');		// str は "h??lo"

Dでは、自然なスライス構文を使います:

char[5] str = "hello";
str[1..3] = '?';		// str は "h??lo"

値 vs 参照

C++の文字列は、STLPortで実装されているように、 値のセマンティクスを持ち 0終端 です。[ 後者は実装依存ですが、STLPort がもっとも広く使われている実装のようです。 ] このことと、 ガベージコレクションが無いことをあわせると、幾つかの結論が導かれます。まず どの文字列オブジェクトも、文字列データのコピーを自分用に作らなくてはなりません。 文字列データの’所有者’が削除されると全ての参照が無効になるため、 ’所有者’を注意深く追跡する必要があります。文字列を値として扱うことで dangling reference 問題を避けようと思ったら、 メモリ割り当て/コピー/解放 のオーバーヘッドが大量に発生します。 次に、0終端であることは、文字列が他の文字列を参照できないことを示します。 静的データセグメントやスタック中などなど... を参照することができません。

D言語の文字列は参照型で、メモリはガベージコレクトされます。 これは、文字列データでなく参照だけをコピーすれば済むことを意味しています。 Dの文字列は静的データセグメントやスタック内のデータ、 他の文字列やオブジェクトの一部分、 ファイル用のバッファなどを直接参照できます。 プログラマが文字列データの '所有者' を追跡する必要もありません。

当然の質問として、複数のDの文字列が一つの同じ文字列データを参照するのなら、 そのデータが書き変わったときにどうなるの? とお考えでしょう。 この場合、全ての参照は書き変わったデータを指すことになります。 これもこれで影響がありますが、copy-on-write の慣習を守ってコーディングすることで回避できます。copy-on-write とは要するに、文字列を書き換える必要があるときは、 まず内容をコピーしてから書き込む、ということです。

Dの文字列が参照型のみでガベージコレクトされる、ということは、 結果として、 例えばlzw圧縮のような文字列操作を多数繰り返す場合の メモリ消費と速度の両面での効率改善につながっています。

ベンチマーク

小さなユーティリティ、テキストファイル中の各単語の出現回数を数える 語数カウンタを見てみましょう。D では、こんな感じになります:

import std.file;
import std.stdio;

int main (char[][] args)
{
    int w_total;
    int l_total;
    int c_total;
    int[char[]] dictionary;

    writefln("   lines   words   bytes file");
    for (int i = 1; i < args.length; ++i)
    {
	char[] input;
	int w_cnt, l_cnt, c_cnt;
	int inword;
	int wstart;

	input = cast(char[])std.file.read(args[i]);

	for (int j = 0; j < input.length; j++)
	{   char c;

	    c = input[j];
	    if (c == '\n')
		++l_cnt;
	    if (c >= '0' && c <= '9')
	    {
	    }
	    else if (c >= 'a' && c <= 'z' ||
		c >= 'A' && c <= 'Z')
	    {
		if (!inword)
		{
		    wstart = j;
		    inword = 1;
		    ++w_cnt;
		}
	    }
	    else if (inword)
	    {   char[] word = input[wstart .. j];

		dictionary[word]++;
		inword = 0;
	    }
	    ++c_cnt;
	}
	if (inword)
	{   char[] w = input[wstart .. input.length];
	    dictionary[w]++;
	}
	writefln("%8s%8s%8s %s", l_cnt, w_cnt, c_cnt, args[i]);
	l_total += l_cnt;
	w_total += w_cnt;
	c_total += c_cnt;
    }

    if (args.length > 2)
    {
	writefln("--------------------------------------%8s%8s%8s total",
	    l_total, w_total, c_total);
    }

    writefln("--------------------------------------");

    foreach (char[] word1; dictionary.keys.sort)
    {
	writefln("%3d %s", dictionary[word1], word1);
    }
    return 0;
}

(巨大なファイルを扱うために、バッファ付きのファイルIOを使う 別の実装 もあります。)

二人の方が、C++の標準テンプレートライブラリを用いて C++ 版を実装してくれました。 wccpp1wccpp2 です。 入力ファイル alice30.txt は”不思議の国のアリス”の文章です。 Dコンパイラ dmd と C++コンパイラ dmc, は同じ最適化ルーチンとコード生成ルーチンを使っているため、 最適化の性能などによらず、 より確実に言語自体の効率を突きあわせて比較ができます。 テストはWinXP機で実行されました。 dmcではテンプレートの実装として STLport を使っています。

プログラム コンパイルオプション コンパイル時間 実行オプション 実行時間
D wc dmd wc -O -release 0.0719 wc alice30.txt >log 0.0326
C++ wccpp1 dmc wccpp1 -o -I\dm\stlport\stlport 2.1917 wccpp1 alice30.txt >log 0.0944
C++ wccpp2 dmc wccpp2 -o -I\dm\stlport\stlport 2.0463 wccpp2 alice30.txt >log 0.1012

以下のテストはLinuxで実行しました。再び、最適化部とコード生成部を共有する Dコンパイラ(gdc) とC++コンパイラ(g++)を使っています。 動作環境は、Pentium III 800MHz 上の RedHat Linux 8.0 と、 gcc 3.4.2 です。 Linux版の Digital Mars D compiler(dmd) も比較に含まれています。

プログラム コンパイルオプション コンパイル時間 実行オプション 実行時間
D wc gdc -O2 -frelease -o wc wc.d 0.326 wc alice30.txt > /dev/null 0.041
D wc dmd wc -O -release 0.235 wc alice30.txt > /dev/null 0.041
C++ wccpp1 g++ -O2 -o wccpp1 wccpp1.cc 2.874 wccpp1 alice30.txt > /dev/null 0.086
C++ wccpp2 g++ -O2 -o wccpp2 wccpp2.cc 2.886 wccpp2 alice30.txt > /dev/null 0.095

以下のテストは gdc で、PowerMac G5 2x2.0GHz マシン上の MacOS X 10.3.5 と gcc 3.4.2 で行いました。 (時間はやや不正確です。)

プログラム コンパイルオプション コンパイル時間 実行オプション 実行時間
D wc gdc -O2 -frelease -o wc wc.d 0.28 wc alice30.txt > /dev/null 0.03
C++ wccpp1 g++ -O2 -o wccpp1 wccpp1.cc 1.90 wccpp1 alice30.txt > /dev/null 0.07
C++ wccpp2 g++ -O2 -o wccpp2 wccpp2.cc 1.88 wccpp2 alice30.txt > /dev/null 0.08

wccpp2 by Allan Odgaard

#include <algorithm>
#include <cstdio>
#include <fstream>
#include <iterator>
#include <map>
#include <vector>

bool isWordStartChar (char c)	{ return isalpha(c); }
bool isWordEndChar (char c)	{ return !isalnum(c); }

int main (int argc, char const* argv[])
{
    using namespace std;
    printf("Lines Words Bytes File:\n");

    map<string, int> dict;
    int tLines = 0, tWords = 0, tBytes = 0;
    for(int i = 1; i < argc; i++)
    {
	ifstream file(argv[i]);
	istreambuf_iterator<char> from(file.rdbuf()), to;
	vector<char> v(from, to);
	vector<char>::iterator first = v.begin(), last = v.end(), bow, eow;

	int numLines = count(first, last, '\n');
	int numWords = 0;
	int numBytes = last - first;

	for(eow = first; eow != last; )
	{
	    bow = find_if(eow, last, isWordStartChar);
	    eow = find_if(bow, last, isWordEndChar);
	    if(bow != eow)
		++dict[string(bow, eow)], ++numWords;
	}

	printf("%5d %5d %5d %s\n", numLines, numWords, numBytes, argv[i]);

	tLines += numLines;
	tWords += numWords;
	tBytes += numBytes;
    }

    if(argc > 2)
	    printf("-----------------------\n%5d %5d %5d\n", tLines, tWords, tBytes);
    printf("-----------------------\n\n");

    for(map<string, int>::const_iterator it = dict.begin(); it != dict.end(); ++it)
	    printf("%5d %s\n", it->second, it->first.c_str());

    return 0;
}