な〜にがStrongTypedefじゃ

この記事はC++ Advent Calendar 201220日目の記事です.

導入

ある型に別名を付けるtypedefに対して,ある型を元に新しい型を作る機能をstrong typedefと言います.もちろんC++の仕様としてそのようなものがあるわけではなく,Boost.Serializationにマクロとして実装されているほか,同様のマクロによる実装がいくつかあります.

なぜこのようなものが必要かという話ですが,世の中にはグーグル=センセイという実際博識なセンセイがいらっしゃいますので,氏にお尋ねいただくのがよろしいかと思います.

ところで,Boost.Serializationにあるstrong typedefには制限があり,適用範囲が組み込み型に限られるのです.その他の実装でも,元となる型との間で暗黙変換を許可する/しないを指定する術がなく,実装によって決められてしまいます(知ってる限りの実装においては).私は変換だけでなく,ありとあらゆる元の型のメンバについて公開する,しないの指定をしたいと思いました.

思ったので作りました.

できました.ここにあります.

https://github.com/dechimal/desalt/

おおざっぱな機能の紹介

はじめに

以下のコードは全て

#include <vector>
#include <iostream>
#include <desalt/newtype.hpp>

があるものとします.

最小の定義

試しにvectorを元に新しい型を作ります.

DESALT_NEWTYPE(ivector, std::vector<int>,
    as_base
);

これでstd::vectorを元にivectorという新しい型を定義しました.この型には次の操作ができます.

  • std::vectorからivectorの新しいオブジェクトを作る
  • コピーする
  • ivectorオブジェクトをas_baseメンバ関数によって明示的にvectorとして扱えるようにする
  • 破棄する
元のクラスのメンバをそのまま使う

これだけやったら別にマクロいらんやろ!!!と思ったので,もっと機能を付け足しました.

DESALT_NEWTYPE(ivector, std::vector<int>,
    as_base,
    begin,
    end
);

これでbeginとendが呼べるようになりました.本当ですか?試してみましょう.

int main() {
    ivector iv(std::vector<int>{1,2,3});
    for (auto i : iv) {
        std::cout << i << '\n';
    }
    std::vector<int> & v = iv.as_base();
}

range based for使うために必要なものは,iteratorを返すbeginとendです.もしこれがコンパイルできないなら,今すぐそのようなザコなコンパイラは捨てましょう.

コンストラクタもそのまま使う

上のマクロを見れば分かるように,メンバの名前を書けば,元のクラスのメンバを呼び出せるようになります.ところで,やはり元のクラスのコンストラクタもそのまま使いたいと思うこともあるでしょう.やりましょう.

DESALT_NEWTYPE(ivector, std::vector<int>,
    as_base,
    begin,
    end,
    this
);

int main() {
     ivector iv1{1,2,3};
     ivector iv2(iv1.begin(), iv2.end());
}

this とだけ書けばstd::vectorの全てのコンストラクタがivectorでも余裕のよっちゃんで使えるようになります.

テンプレート

ところでさいきんtemplate aliasesってのがあるじゃないですか.template using vec = std::vector; みたいなの.これみたいにtemplateに対しても別名宣言できたりは…するんですねこれが.

template<typename T>
DESALT_NEWTYPE(dynamic_array, std::vector<T>,
    as_base,
    begin,
    end,
    this
);

int main() {
    dynamic_array<int> di{1,2,3};
    for (auto i : di) {
        std::cout << i << '\n';
    }
}
手動で機能を追加する

最後の手段として,自分で勝手にメンバとかを付け加えたりもできます.

template<typename T>
DESALT_NEWTYPE(dynamic_array, std::vector<T>,
    as_base,
    begin,
    end,
    this,
    new (
        void f() const { std::cout << "hoge\n"; }
    )
);

int main() {
    dynamic_array<int> di{1,2,3};
    for (auto i : di) {
        std::cout << i << '\n';
    }
    di.f();
}
その他

で,本当なら他にも機能あったんですが,まだほとんどの場合正しく動かないので使えません(実装を読んで挙動を理解できるなら使ってもよい).

書式

DESALT_NEWTYPE(identifier, underlying_type, convert_to_base_function_name);
DESALT_NEWTYPE(identifier, underlying_type, convert_to_base_function_name, ...);
  • identifier : 新しい型の名前
  • underlying_type : 元になる型の名前
  • converto_to_base_function_name : 元の型の値を取り出す用の関数の名前

... は以下の要素のどれかをカンマで区切って並べたN個の列 (N ≦ 64)

this // using underlying_type::underlying_type;
member_name // using underlying_type::member_name;
friend function_name // friend function_name;
friend class class_name // friend class class_name;
typename type // using typename underlying_type::type;
new ( ... ) // ... をそのまま定義に付け加える