D言語でもboost::anyをする.(任意の型の値を入れられるコンテナをつくる)

きっかけ

Twitterかどこかで,Type Erasureなる文字をみた. そういえば,以前JavaGenericsの実装がType Erasureを用いてるみたいなことを調べたことがあったけど(あれはListみたいなものがListになっているみたいなものっぽくて,型変数が消えた状態で識別されるみたいな話っぽいけど),Type Erasureってなんだろうって思ってぐぐってみた

そうすると,次の記事がでてきた猿でも分かった~型消去技法とは - 418 I'm a teapot

なるほど,要するにダックタイプができるってことっぽい.

もう少し詳しく書くと,2つのクラスに継承関係がなくても,例えば共通の関数fを実装しているということがわかれば,それらを中身の差異はあれど,fという点においては共通であるから次のようなことができる.

import std.stdio;

/*
   AとBは独立の型であって,継承関係はない.
 */
class A {
  void f() {
    writeln("A#f is called!");
  }
}

class B {
  void f() {
    writeln("B#f is called!");
  }
}

// A, Bはともに,void f();という共通のシグネチャをもっているから,void f()だけをもつプレースホルダーをつくる.
abstract class Holder {
  abstract void f();
}

// 実際に値を格納するコンテナをつくる.
class Holder_Container(T) : Holder {
  T obj;
  this (T obj) { this.obj = obj; }
  override void f() { obj.f(); }
}

void main() {
  Holder[] objs;

  // Holder_ContainerはHolderを継承しているからHolderの配列に追加することが可能
  objs ~= new Holder_Container!(A)(new A); // これは末尾に追加する演算
  objs ~= new Holder_Container!(B)(new B);

  foreach (obj; objs) {
    obj.f; // それぞれ,A.fとB.fを呼び出している.
  }  
}

C++だとvirtualキーワードを用いていたけど,Dではvirtualはないので,abstractにする.(こうすると,プレースホルダにもなる.)

こうすると,HolderというAとBをつなぐインターフェースのようなものを介して扱うことができて,静的な型がついていてもダックタイプができる.

で,書くまでもないけど,継承関係がない場合を上に書いたけど,ダックタイプでもなんでもない普通の継承関係がある場合は次のようにかけて,当然ちゃんと動く.(明示的にインターフェースを書く)

import std.stdio;

interface HasF {
 void f();
}

class A : HasF {
  void f() {
    writeln("A#f  is called!");
  }
}

class B : HasF {
  void f() {
    writeln("B#f  is called!");
  }
}

void main() {
  HasF[] hf;

  hf ~= new A; 
  hf ~= new B;

  foreach (e; hf) {
    e.f;
  } 
}

これは基本なので説明するまでもない.

実装する.

とりあえず,先程の記事を読み進めると,boost::anyというものがあって,これは任意の型(実際は制約があるっぽい?けど)を入れることのできる型で,これも型消去を用いて実装されているらしい.

なるほど.これはDで実装するしかないよね,となった.

D言語には似たようなものとしてstd.variantにVariantがあるけど,あれは格納したい型の最大のサイズを計算して,その大きさのunionを持つということで値を保持している. 今回作るものはそういうものではなくて,型は静的に解決されている.

とりあえず,boost::anyのコードをみるのもいいけど,実際にC++で実装している記事を見たので,それをDに持ってくることにした.

boost::anyを実装してみる - (void*)Pないと

で,実装した.

import std.traits,
       std.meta;

class Any {
  private {
    /*
      インターフェース
     */
    abstract class _AnyBase {
      abstract TypeInfo type();
      abstract _AnyBase clone();
    }

    /*
      実際に値を保持するクラス
     */
    class _Any(T) : _AnyBase {
      T value;

      this (T value) {
        this.value = value;
      }

      override TypeInfo type() const {
        return typeid(T);
      }

      override _AnyBase clone() {
        return new _Any!T(this.value);
      }

      ~this () {}
    }

    /*
        内部で値を保持する(こいつがミソ) (ある型の値を持っているのに型情報が型に含まれてないのがポイント!)
     */
    _AnyBase obj;
  }

  public {
    /*
      コンストラクタ
      - 引数が無いもの
      - 一般の値を引数にできるもの 
      - Any型が引数のもの
      の3種類.
     */
    this () {}
    
    this(T)(T value) {
      this.obj = new _Any!T(value);
    }

    this(Any any_obj) {
      if (any_obj.obj !is null) {
        this.obj = any_obj.obj.clone();
      } else {
        this.obj = null;
      }
    }

    // TがAnyとは無関係であるということを言わないと,opAssignは定義できないので
    Any opAssign(T)(T value) if (!is(T == Any)) {
      delete this.obj;
      this.obj = new _Any!T(value);
      return this;
    }

    // この関数を用いて値を取り出す.
    T castTo(T)() {
      if (this.obj is null) {
        throw new Exception("this object has no value");
      }
      if ((cast(_Any!T)this.obj) is null) {
        throw new Exception("can not cast into incompatible type");
      } else {
        return (cast(_Any!T)this.obj).value;
      }
    }

    // 型情報を返す.
    TypeInfo type() {
      return this.obj.type;
    }
  }
}

import std.stdio;

class K {
  int value;
  this (int value) {
    this.value = value;
  }
}

void main() {
  Any a = new Any;

  a = 10;
  writeln(a.type);
  writeln(a.castTo!int);

  a = [1, 2, 3, 4, 5];
  writeln(a.type);
  writeln(a.castTo!(int[]));

  a = new K(123);
  writeln(a.type);
  writeln(a.castTo!(K).value);
}

えーと,結論からいうと これがなんでできているかというと,

内部にはまず,2つのクラスがある._AnyBase型と_Any!T型である. そして,_Any!T_AnyBaseを継承している.ここで型変数Tが消えていることに注目.

つまり,実際に値を保持するobjは_AnyBase型であり,型情報(型変数)をもたない! つまり,どんな型Tがきて_Any!Tがつくられても,_AnyBaseはそれを受け入れることができるから,どんな型でも保持できるってワケ

まとめると

  • 実際に値を保持するのは_AnyBase objで,これは内部ではobj = new _Any!T(T型の値)というように使われる(_Any!T_AnyBaseを継承しているので,_AnyBaseに入れることができる)
  • _AnyBaseは中身(値であったりその型)には言及していない.つまり,どんな_Any!Tもうけとれる ← ここがミソ

まとめ

すごく久しぶりにブログを書いたので,ブログの書き方を忘れてしまったので,すごい適当な記事になってしまった.