dlang
D言語Day 16

C++erがD言語を使ってみて感激した機能

More than 3 years have passed since last update.

D言語を使う理由とこの記事の趣旨

なんかC++がだるくなったからー。

ネイティブコードのライブラリの作成をメインに趣味をしているとC++でのライブラリ作成につらみが出てくる。よい言語はないかと探していてD言語に行き当たった。

D言語を勉強していて「これは便利!楽!なんでC++はこうなっていないんだ!」という言語機能がたくさんあったので、C++出身の人間からみたD言語の良いところをまとめてみる。

個人の感想なので他にもまだまだD言語の機能はある。

D言語の世界へようこそ。

基本データ型 : サイズは言語仕様になっている

Types

int, short, doubleなど。

Dではこれらの基本データ型のバイト長は言語使用により定められている。C/C++では基本データ型のバイト長は処理系依存であり、指定長の型を使うには stdint.h/cstdint を include する必要がある。

らぶりー。

alias, alias this : いろいろなものに別名をつける

Declarations / Alias Declarations
Classes / Alias This

シンボルに別名をつける機能。typedefのようにも使えるが、変数、関数、キーワードがさすもの(this)に別名がつけられる。

メンバ変数に別名をつけたり・・・

import std.stdio;

class Matrix
{
public:
    this(int c, int r)
    {
        cols = c;
        rows = r;
    }

    double data[]; // ...

    int cols;
    int rows;

    alias width = cols;
    alias height = rows;
}

void main()
{
    auto m = new Matrix(3,5);
    // Matrixのメンバcols, rowsに別名width, heightでアクセスできる
    // C++であれば inline の参照返しで最適化されることを期待する以外に
    // 同様の別名の付与はできない。
    writeln(
        m.cols, ", ", m.rows, ", ", m.width, ", ", m.height);
}

alias this でthisでメンバにアクセスしたりとかできる。

import std.stdio;

class SuperArray
{
public:
    int data[10];
    alias data this;

    int sum()
    {
        int r = 0;
        foreach(int i; 0 .. 10)
        {
            r += data[r];
        }
        return r;
    }
}

void main()
{
    auto a = new SuperArray();
    writeln(a.length, ", ", a.sum()); // メンバのプロパティを自身のように使用できる。
}

らぶりー。

with : コード上で集中しているものに集中する

Statements / With Statement

VisualBasic の with と同じようなもの。Cライブラリの構造体をいじったりする時にとても便利。

import std.stdio;

struct Point
{
    int x;
    int y;
}

void main()
{
    Point p = {3,4};

    with(p)
    {
        writeln(x);
        writeln(y);
    }
}

foreach, foreach_reverse : 便利なループ

Statements / For Statement
Statements / Foreach Statement
Statements / Foreach Range Statement

C++11から for の記述の柔軟性が増したが、D言語でもループの記述に同等の機能を提供している。

import std.stdio;

void main()
{
    // C++11 の range based for のような記述ができる
    string text = "this is a pen.";
    foreach(char c; text)
    {
        write(c, "|");
    }
    writeln();

    // int を伴うことで index を同時に取得する書き方もできる
    foreach(int i, char c; text)
    {
        writeln(i, ": ", c);
    }

    // foreach_reverse で逆順にループすることができる
    foreach_reverse(char c; text)
    {
        write(c);
    }
    writeln();

    // foreach range 文で範囲を記述することができる
    foreach(int i; 0 .. 10)
    {
        write(i, " ");
    }
    writeln();
}

switch : 柔軟なswitch

Statements / Switch Statement

C++より少しだけ優しいんじゃ。

import std.stdio;

void main()
{
    {
        int a = 4;
        int c1 = 3, c2 = 4, c3 = 5;

        switch(a)
        {
        case c1: // コンパイル時に値を決定できればcaseに使用できる
            writeln("a is c1.");
            break;
        case c2, c3:
            writeln("a is c2 or c3.");
            break;
        default:
            writeln("default!");
            break;
        }
    }

    {
        switch(a)
        {
        case "your text": // 文字列をcaseに使用できる
            writeln("a is c1.");
            break;
        case "my text", "her text":
            writeln("a is c2 or c3.");
            break;
        default:
            writeln("default!");
            break;
        }
    }
}

auto : おなじみのauto

Declarations / Implicit Type Inference

説明不要。C++11 ~ と同様に書くことができる。

import std.stdio;

void main()
{
    auto i = 1;
    auto s = "my string";

    writeln(typeof(i).stringof);
    writeln(typeof(s).stringof);
}

import expression : ファイルのコンパイル時読み込み

Expressions / Import Expressions

dubプロジェクトのツリーを以下のように作る。

.
├── dub.json
├── resources
│   └── my_text.txt
└── source
    └── app.d

2 directories, 3 files

dub.json に stringImportPaths を記述する。

{
    "name": "garage",
    "description": "A minimal D application.",
    "copyright": "Copyright © 2014, yusuke",
    "authors": ["yusuke"],
    "dependencies": {
    },
    "stringImportPaths" : ["resources"]
}

以下のコードで my_text.txt の内容をコンパイル時に string で読み込むことができる。

import std.stdio;

void main()
{
    auto str = import("my_text.txt");
    writeln(str);
}

ちょっとしたリソース管理に便利。vibe.d はこの仕組みを使ってテンプレートの処理を行っている。

property : 便利なプロパティ

Properties
Enums enum にもプロパティがある

C++であれば型に関する情報は limits などで個別に異なる記法で取得するが、D言語では統一的な書き方で取得することができる。

またEnum型のプロパティはたとえば T.max を得たときに元となる整数型の最大値ではなくそのEnum型がとりうる最大のメンバーを返すなど、Enum型に特化した仕様になっている。

便利。

import std.stdio;

// 関数テンプレート、整数型のプロパティの一部を表示する
void printProperties(T)()
    if( __traits(isIntegral, T) && !is(T == enum) )
{
    writeln("T.stringof: ", T.stringof);
    writeln("T.sizeof: ", T.sizeof);
    writeln("T.init: ", T.init);
    writeln("T.min: ", T.min);
    writeln("T.max: ", T.max);
    // などなど
}

// 浮動小数点数型のプロパティの一部を表示する
void printProperties(T)()
    if( __traits(isFloating, T) )
{
    writeln("T.stringof: ", T.stringof);
    writeln("T.sizeof: ", T.sizeof);
    writeln("T.init: ", T.init);
    writeln("T.min: ", T.min_normal);
    writeln("T.max: ", T.max);
    writeln("T.infinity: ", T.infinity);
    writeln("T.nan: ", T.nan);
    writeln("T.epsilon: ", T.epsilon);
    // などなど
}

// Enum型のプロパティを表示する
void printProperties(T)()
    if( is(T == enum) )
{
    writeln("T.stringof: ", T.stringof);
    writeln("T.sizeof: ", T.sizeof);
    writeln("T.init: ", T.init);
    writeln("T.min: ", T.min);
    writeln("T.max: ", T.max);
}

enum Jobs
{
    worker,
    expert,
    exective
}

void main()
{
    printProperties!(int);
    printProperties!(double);
    printProperties!(Jobs);
}

出力

T.stringof: int
T.sizeof: 4
T.init: 0
T.min: -2147483648
T.max: 2147483647
T.stringof: double
T.sizeof: 8
T.init: nan
T.min: 2.22507e-308
T.max: 1.79769e+308
T.infinity: inf
T.nan: nan
T.epsilon: 2.22045e-16
T.stringof: Jobs
T.sizeof: 4
T.init: 0
T.min: 0
T.max: 2

in, out, body / unittest : テストの記述の標準

in, out, body

Contract Programming

D言語では関数をin, out, bodyのブロックに分けて定義することができる。inとbodyのみ、outとbodyのみ、bodyキーワードを使用せずに一般的な定義のみをすることもできる。

inブロック、outブロックに記述された定義はコンパイラのdebugコンパイル時にコンパイルされ、releaseコンパイル時にはコードは無視される。inブロックは事前条件の記述であり引数のチェックを行う。bodyブロックが実行される前に実行される。outブロックは事後条件の記述であり戻り値のチェックを行う。bodyブロックが実行された後に実行される。

記述位置がパターン化されたアサーションと考えればよい。この機能は契約プログラミングを実現するためのものであるが、あまり気にせず便利に安全なコードを書くのに使えばよいと思う。

契約プログラミングに関する機能には他に不変条件をチェックする invariant() がある。

import std.math;
import std.stdio;

double someFunction(double d1, double d2)
in
{
    // 事前条件の記述
    assert(d1 >= 0);
    assert(d2 >= 0);
}
out(r)
{
    // 事後条件の記述
    // 関数の戻り値をrとして、その内容をチェックする
    assert(r != double.nan && r != double.infinity);
}
body
{
    // 関数の本体
    return sqrt(d1) + sqrt(d2);
}

void main()
{
    writeln(someFunction(1,2));
    writeln(someFunction(-1,2));
}

unittest

Unit Tests

D言語ではコードのさまざまな場所に unittest を記述することができる。unittest 内のコードは実行バイナリの静的初期化が実行された後、 main() が実行される前に実行される。

個々の unittest の実行順は

unittest を実行するには dub においては targetType を executable にして、 dub build --build=unittests を実行する。ライブラリの開発においては仮の main() を作成しておき、main()とunittestをコンパイルするコンフィギュレーションを作成しておくと便利である。

import std.stdio;

int add(int i1, int i2)
{
    // バグのある関数
    return i1 + i2 + 1;
}

void main()
{
    // 本来のアプリケーションのコードと独立して簡単なテストを記述できる
}

unittest
{
    // このコードはunittestでコンパイルされた時のみ実行される
    writeln("this is unittest code");
    assert(add(1,2) == 3); // error
    writeln("test clear");
}

今回書かなかったこと

今回はD言語を勉強していて「あ、これ便利だな、C++よりぬるくていいな」という言語機能を並べてみた。上に挙げたキーワードには他にもさまざまな機能があるし、挙げていない機能もたくさんある。

特にテンプレート関連の機能はそれで一本ブログが書けてしまうのでまるまる省略した。特にtraits関係のコア機能・ライブラリが充実していること、特殊化の記述が便利であることがD言語の特徴であると思う。

ぜひともD言語を勉強してC++の苦行から脱していただきたい。