Help us understand the problem. What is going on with this article?

C++プログラマにお届けしたいD言語のテンプレート

More than 3 years have passed since last update.

はじめに

 今年の記事は,タイトルからもわかるようにC++はちょっと触ったことあるけどD言語は知らないよって人向けです.C++のテンプレートは,初心者,もしくは中級者にとっても非常に難しい機能です.D言語はC++を参考に作られた言語であり,D言語にもC++相当のテンプレート機能があります.しかしながら,D言語のテンプレートは非常に簡単で,初心者にとっても学びやすいと思います.そういうことで,今回はD言語のテンプレートを紹介します.

 あと,この記事は豊橋技術科学大学コンピュータクラブ(東モ39a)がコミックマーケットC89にて頒布する部誌用に書いたものです.当日はサークル作のゲームもあるようなので,部誌と合わせてぜひぜひ(宣伝).

コードのスタイルについて

 この文章に含まれているコードのうち,いくつかは非常に変なコーディングスタイルとなっているかもしれません.これは紙に印刷するという制約から生じるものですので,どうか許してください.

テンプレートとは

 テンプレートについて解説するためには,そもそもテンプレートとはどのような機能で,どのような役割を持っているのかを説明する必要があります.関数がある理由は,ある機能や手順を再利用するためでした.たとえば,int型を要素として持つ配列int[] arrの要素の総和を計算する関数は次のように定義できます.

int sum(int[] arr) {
    int res;        // Dではデフォルトで0初期化
    foreach(e; arr) // 配列を順にイテレート
        res += e;

    return res;
}

このようにsum関数を定義しておけば,int[]型の総和を計算したいとき,sum関数を呼び出すだけで良くなります.

 もしsum関数をlong[]float[]などで使いたくなった場合はどうしましょう?
愚直に各型について関数を定義し直しますか?では,double[]や複素数型cfloat[]も実装しますか?もしくはbyte[]short[]は?そこでテンプレートの登場です.

関数テンプレート

 sum関数を任意の型に拡張しましょう.非常に簡単です.

import std.stdio;

T sum(T)(T[] arr) {
    T res = 0;
    foreach(e; arr)
        res = res + e;

    return res;
}

void main() {
    int[] arr = [1, 2, 3];
    writeln(sum(arr));      // 6
}

 どこが変わったかわかりますよね?Tという型パラメータを導入しました.これは,関数名パラメータの間に(T)と書くだけです.あとは,浮動小数点数のことを考慮してresをゼロ初期化しました.関数テンプレートを呼び出す場合は,Tは自動的に引数から推論されます.

 もし,複数の型パラメータが必要であれば次のように書くだけです.
非常にシンプルでしょう?

void copy(E1, E2)(E1[] src, E2[] dst)       // srcからdstにコピー
in { assert(src.length == dst.length); }    // 実行時にサイズチェック
body {
    foreach(i, e; src)
        dst[i] = e;     // コピー
}

ダックタイピングと制約

 ダックタイピングとは,

If it walks like a duck and quacks like a duck, it must be a duck. (もしそいつがアヒルのように歩き,アヒルのように鳴くのであれば,そいつは間違いなくアヒルだ)

というなんとも面白い言葉が元になった用語です.ダックタイピング自体の意味は,「ある型にいて,所望する動作をしそうであれば,その型は所望する型である」という意味です.もっと具体的に言いましょう.たとえば,sum関数は「ある型について二項演算子+を用いて加算を表せるような型」について関数を実行可能です.

 テンプレートは,インスタンス化される際に初めて型レベルでコードの正しさがチェックされます.つまり,ある型T+演算子を持っているかどうかや,E2E1の値がコピー可能かどうかはインスタンス化される時にはじめてチェックされます.この結果,C++やD言語のテンプレートは静的ダックタイピングをサポートできています.静的ダックタイピングにより,素晴らしく簡素な記述であらゆる物事を統一して扱えます.

 C++では,インスタンス化する際に,「その型が所望する動作をしそう」かどうかを判定できず,テンプレートにより発生するエラーは非常に奇怪でした.D言語では,テンプレート制約というものを導入することで,インスタンス化される前に型チェックが行えます.たとえば,std.traitsで定義されているisAssignableを使うと,ある型に別の型を代入できるかチェックできます.

import std.traits;

void copy(E1, E2)(E1[] src, E2[] dst)
if(isAssignable!(E2, E1))   // E2へE1が代入できるかチェック
in{ assert(src.length == dst.length); }
body {
    foreach(i, e; src)
        dst[i] = e;
}

 ある型について,加算可能かどうかチェックするには2通りの方法があります.
一つ目は,__traits(compiles, expr)を使用してexprがコンパイル可能かチェックする方法です.二つ目はis(typeof(expr))というイディオムです.
is(type)typeが不正な型の場合falseとなります.また,typeof(expr)exprの型を返します.したがって,is(typeof(expr))exprが不正な型を持つ場合にはfalseとなり,そうでない場合はtrueとなります.つまり,型レベルでの正しさのチェックを行いたい場合にis(typeof(expr))を使用します.

 しかし大抵の場合,両方の方法で同じ結果が得られるので,どちらを使用しても良いと思いますが,個人的にはis(typeof(expr))の方が好きです.なぜかというと,テンプレート制約は「その関数を呼び出すことができるか」のチェックであるからです.呼び出し可能かどうかのチェックは型レベルのチェックですから,私はis(typeof(expr))を用いるほうが良いと思っています.

import std.traits;

T sum(T)(T[] arr)                           // 合計を計算
// if(__traits(compiles, arr[0] + arr[1]))  // こっちでも良い
if(is(typeof(arr[0] + arr[1]))) {
    T res = 0;
    foreach(e; arr)
        res = res + e;

    return res;
}

 ある性質を持つものの扱い方を統一することで,ダックタイピングが行え,ライブラリの再利用性が向上します.ダックタイピングはD言語のさまざまなライブラリで活用されています.配列を拡張した概念であるレンジ(Range)はその典型的な例です.入力レンジ(Input-Range)はシーケンシャルアクセス可能なものを表します.型Tがについて,次のようなコードのコンパイルが通る場合,Tは入力レンジです.

T t;
auto v = t.front;   // 先頭要素を取得可能
t.popFront();       // レンジの先頭を次の要素に進める
if(t.empty) {}      // レンジに一つも要素がないかチェック可能

本来のテンプレートと冠名テンプレート

 さて,関数テンプレートの書き方を紹介しましたが,実はあの構文は糖衣構文です.本来,D言語のテンプレートは以下の様な構文で書きます.

// 一般的なテンプレートの例
template Pair(T, U)
{
    struct Struct { T a; U b; }     // いろいろな宣言を
    class Class { T a; U b; }       // テンプレート内に含められる

    Struct makeAsStruct(T a, U b) { return Struct(a, b); }
    Class makeAsClass(T a, U b) {
        auto c = new Class;
        c.a = a; c.b = b;
        return c;
    }
}

// sum関数の例
template sum(T)
if(is(typeof((T a){ a = a + a; })) || is(typeof((T a){ a += a; }))) {
    T sum(T[] arr) {
        T res = 0;
        foreach(e; arr)
            res = add(res, e);

        return res;
    }

    // a + bか,a += bで実行可能な方を選ぶ
    T add(T a, T b) {
      // コンパイル時に条件分岐
      static if(is(typeof(a + b) : T))  // a+bが型的に正しく,結果の型はTであるか?
        return a + b;
      else{ a += b; return a; }
    }
}

void main()
{
    import std.stdio;

    int[] arr = [1, 2, 3];

    // Pairをintとfloatでインスタンス化し,内部で宣言されたものを使う
    Pair!(int, float).Struct pair = Pair!(int, float).makeAsStruct(1, 1);

    // sumのように,テンプレート内にテンプレート名と同一名の宣言がある場合,簡略できる(Eponymous template)
    writeln(sum!int(arr));

    // さらに,関数テンプレートだから,引数からテンプレート引数を推定してくれる
    writeln(sum(arr));
}

 sum関数のように,テンプレート名とそのテンプレート内部で宣言されたものの識別子が同じ場合,Eponymous templateといます.非正式で局所的ですが,Eponymous templateは日本語では「冠名テンプレート」と呼ばれることがあります.冠名テンプレートは,関数や構造体,共用体,定数宣言,クラス,インターフェース,alias宣言の場合,糖衣構文として次のように書けます.

T foo(T)(T a) { return a; }     // 関数
// template foo(T) { T foo(T a) { return a; } }

struct FooS(T) { T a; }         // 構造体
// template FooS(T) { struct FooS{ T a; } }

union FooU(T) { T a; }          // 共用体
// template FooU(T) { union FooU { T a; } }

enum FooConst(T) = T.init;      // 定数宣言
// template FooConst(T) { enum FooConst = T.init; }

class FooC(T) { T a; }          // クラス
// template FooC(T) { class FooC { T a; } }

interface FooI(T) { T foo(); }  // インターフェース
// template FooI(T) { interface FooI { T foo(); } } 

alias FooA(T) = T;              // alias宣言
// template FooA(T) { alias FooA = T; }

テンプレートパラメータ

 今までテンプレートのパラメータには型のみを用いてきましたが,実際には整数値や文字列,任意のシンボルを引数に取れます.たとえば,関数を受け取り,内部でその関数を呼び出すことも可能です

// reduce!((a, b) => a + b)(0, arr)で総和, reduce!((a, b) => a * b)(1, arr)で総乗
T reduce(alias f, T)(T ini, T[] arr)
if(is(typeof(binaryFun!f(ini, ini)) : T)) {
    T res = ini;
    foreach(e; arr)
        res = binaryFun!f(res, e);

    return res;
}

// 文字列 -> 関数変換,sが"a+b"ならa+bを返す
auto binaryFun(string s, T, U)(T a, U b)
if(is(typeof(mixin(s)))) { return mixin(s); }

// 関数をそのまま返す
template binaryFun(alias f)
if(!is(typeof(f) : string)) { alias binaryFun = f; }

// aliasは別名定義なので,変数も参照可能
void setDefaultValue(alias var)() { var = typeof(var).init; }

void main() {
    import std.stdio;
    int[] arr1 = [1, 2, 3];

    writeln(reduce!"a+b"(0, arr1));                 // 6
    writeln(reduce!((a, b) => a + b)(0, arr1));     // 6

    setDefaultValue!arr1;
    writeln(arr1);                                  // []
}

可変長引数テンプレートとコンパイル時リスト

 C++11では,可変長引数テンプレートがサポートされましたが,D言語ではそれ以前から採用されています.また,C++11よりもわかりやすい構文となっているので,理解しやすいでしょう.ここでは,Dの可変長引数テンプレートの強力さを簡単に紹介するために,渡された引数リストの文字列表現を連結して返す関数を実装します.

string asString(T...)(T args) {
    import std.conv;

    string dst;
    foreach(e; args)
        dst ~= to!string(e);

    return dst;
}

void main() {
    import std.stdio;

    writeln(asString(1, 2, 3));         // 123
    writeln(asString(1, "23", 4));      // 1234
    writeln(asString(null, 1));         // null1
}

 可変長のテンプレート引数は<Identifier>...と書きます.ここではT...なのでTが可変長の引数リストになります.このようなものを,「タプル」とか「コンパイル時リスト」といいます.コンパイル時リストはforeach文でイテレート可能で,T[idx]のようにidxがコンパイル時定数であれば要素へのアクセスも可能です.
 コンパイル時リストには,テンプレート引数として渡すことができるものであれば,型以外でも要素に含むことができます.コンパイル時リストの要素が全て型の場合,argsのように仮引数や変数を定義可能で,この仮引数や変数もコンパイル時リストとなります.変数のように定義されたargsなどのようなコンパイル時リストは,次のようにコンパイラにより生成された変数へのaliasとなります.もちろん,コンパイル時リストに詰め込まれたaliasを通して,変数の値を書き換えることも可能です.

// asStringをint, string, intでインスタンス化した場合
// __v1, __v2, __v3はコンパイラにより生成されるため,コード上には実際は存在しない
string asString(int __v1, string __v2, int __v3) {
    alias T = /* (int, string, int)からなるコンパイル時リスト */;
    alias args = /* (__v1, __v2, __v3)からなるコンパイル時リスト */;

    ...
}

struct MyTuple(T...) {
    T vals;

    /+ MyTupleを(int, string, long)でインスタンス化すると,次のようにコンパイラが生成する.
    alias T = /* (int, string, long)からなるコンパイル時リスト */;

    int __v1;
    string __v2;
    long __v3;
    alias vals = /* (__v1, __v2, __v3)からなるコンパイル時リスト */
    +/
}


void main() {
    import std.stdio;

    MyTuple!(int, string, long) v;
    v.vals[0] = 1;
    v.vals[1] = "abc";
    v.vals[2] = 2;

    writeln(v.vals);    // 1abc2
}

 また,std.meta.AliasSeqを使うことで,コンパイル時リストを作ることができます.以下の例では,std.meta.AliasSeqの使い方と,コンパイル時リストのさまざまな性質を紹介しています.

import std.stdio, std.meta;

/+ AliasSeqはstd.meta内部で次のように定義されている
template AliasSeq(TList...) { alias AliasSeq = TList; }
+/

void main() {
    // (int, 1, 2, long, main)からなるコンパイル時リストListの定義
    alias List = AliasSeq!(int, 1, 2, long, main);

    /*
    int
    1
    2
    long
    main()
    */
    foreach(e; List)
        writeln(e.stringof);

    // (byte, short, int, long)からなるコンパイル時リストの定義
    alias TypeList = AliasSeq!(byte, short, int, long);

    // コンパイル時リストは自動的に展開されるので,
    // AliasSeq!(int, 1, 2, long, main, byte, short, int, long)
    // 上と等しい
    alias Concatenated = AliasSeq!(List, TypeList);

    // スライス可能, (long, main, byte, short, int)
    alias Sliced = Concatenated[3 .. $-1];
}

メタプログラミング入門

 再利用性の高いプログラムを書きたい場合,メタプログラミングは素晴らしい効果を発揮します.C++ではテンプレートを用いたメタプログラミングは複雑ですが,D言語では複雑さはある程度緩和され,その強力さは受け継がれています.メタプログラミングは,時々「闇」と言われますが,プログラマにとっては光であることが多いでしょう.本章ではメタプログラミングの中でも,コンパイル時計算,文字列mixin,静的リフレクションを紹介します.

コンパイル時計算

 テンプレートメタプログラミングの一例としてよく出てくるものは,階乗の計算をコンパイル時に行うことでしょう.D言語にはコンパイル時に条件分岐が可能なstatic ifがあるので,簡単に記述可能です.

// テンプレートを用いた階乗の計算
template factorial(uint N) {
  static if(N == 0)
    enum uint factorial = 1;
  else
    enum uint factorial = N * factorial!(N-1);
}

 また,一定の条件を満たした関数はコンパイル時に実行可能なので,次のような関数でもコンパイル時に階乗を求めることができます.

uint factorial(uint N) {
    uint dst = 1;
    foreach(n; 1 .. N+1)
        dst *= n;

    return dst;
}

文字列mixin

 binaryFunで使用しましたが,D言語には文字列mixinという機能があり,コンパイル時定数な文字列をソースコードとして解釈させることができます.Dynamically typedな言語が持つevalとの違いですが,evalは実行時に文字列をパースして評価しますが,mixinはコンパイル時定数な文字列をソースコードとして取り込むため実行時オーバーヘッドがありません.コンパイル時実行可能な関数でD言語のソースコードを生成し,それを文字列mixinすると面白いことができると思いませんか?
 文字列mixinを用いた例として,naryFunの実装があります.naryFunbinaryFunを一般化したもので,文字列から任意個数の引数からなる関数を構築できます.naryFun!"a+b+c"なら引数3つの和ですし,naryFun!"a(b, c, d)"は第1引数を第2, 3, 4引数で呼び出す関数になります.naryFunを実装することは非常に簡単です.

template naryFun(alias fun) if(is(typeof(fun) == string)) {
    auto ref naryFun(T...)(auto ref T args) {
        static assert(T.length <= 26);      // a ~ zの範囲に収まるかチェック
        mixin(generateVariables(T.length)); // 変数a ~ zを宣言するコードをmixin
        return mixin(fun);                  // funをmixin
    }


    // 変数 a ~ z を宣言するコードを生成,コンパイル時実行可能
    string generateVariables(size_t nparam) {
        import std.array, std.format;

        auto app = appender!string();
        foreach(i; 0 .. nparam)         // コード生成
            formattedWrite(app, "alias %s = args[%s];\n", cast(char)(i + 'a'), i);

        return app.data;
    }
}

void main() {
    import std.stdio;

    writeln(naryFun!"a+z"(1,2,3,4,5,6,7,8,9,10,11,12,13,
                          14,15,16,17,18,19,20,21,22,23,
                          24,25,26));               // 27
    writeln(naryFun!"a+b+c"(1, 2, 3));              // 6
    writeln(naryFun!"a"(1, 2, 3));                  // 1
    writeln(naryFun!"a(b)"((int a) => a+1, 2));     // 3
}

静的リフレクション

 リフレクションは,主にDynamically typedな言語やオブジェクト指向言語でよく実装されています.単にリフレクションといえば実行時リフレクションを指し,実行時にオブジェクトがどのようなメソッドやプロパティを持つかチェックしたりできる機能です.D言語では今のところ実行時リフレクションはできませんが,静的リフレクション,つまりコンパイル時リフレクションなら行えます.コンパイル時リフレクションでは,任意の型について,その型が持つメンバ関数の一覧やフィールド(メンバ変数, 非公開を含む)一覧,もしくはモジュールに属するシンボル一覧を取得できたりします.ここでは簡単に,任意の構造体の型Tについて,その型のフィールド一覧を出力してみましょう.

import std.stdio;

void printAllField(T)(T val) {
    foreach(i, ref e; val.tupleof){
        writefln("%s %s = %s;",
            typeof(e).stringof,
            val.tupleof[i].stringof[4 .. $],    // 先頭の "val." をスキップ
            e);
    }
}

void main() {
    static struct S { int a, b; string c; }

    /* 次のように出力される
    int a = 1;
    int b = 2;
    string c = 3;
    */
    printAllField(s);
}

 この例以外にも,std.traits__traitsによってさまざまなことが行えます.言い出すときりがないのと,ココらへんはやりたいことによって全然違うので,興味がありましたらstd.traits__traits機能のドキュメントを読んでみてください.

まとめ

 今回はD言語のテンプレートについて紹介しました.テンプレートは,D言語の重要な機能の一つで,これをマスターすることで素晴らしい生産性を発揮できます.C言語erやC++erの方でもしD言語に興味が湧いたら,早速入門しましょう.D言語を勉強することで,C++の理解につながりますので,C++を理解できない人は,まずD言語で遊んでみると良いと思います.また,その他の言語を使用してる人でも非常に簡単に入門できると思いますのでぜひぜひ.

k3_kaimu
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした