2024年2月10日 13:58 追記: NEWTYPE_DEFINE_NEW_TYPE
マクロを使って型を定義する方法を追加しました。
New Type Idiomと呼ばれるRustの手法の紹介と、同様のことをC++で簡単に行うための自作ライブラリの紹介をしたいと思います。
動機
intやstd::stringのような型は汎用的であるために、他の値と混同しやすい問題があります。例えば以下のようなコードがあるとします。
int get_user_id(const std::string& user_name);
int get_game_id(const std::string& game_title);
PlayInfo get_play_info(int user_id, int game_id);
int game_id = get_game_id("Legend of Zelda");
int user_id = get_user_id("John");
PlayInfo info = get_play_info(game_id, user_id);
このコードには誤りがあります。気付いたでしょうか?
正解はget_play_info
に渡す引数が逆になっていることです。引数名を見ればわかりますね。ですがこのコードは、意味的には誤った呼び出し方をしているにも関わらず、コンパイラはエラーを出しません。なぜなら、ゲームIDもユーザーIDも単なるint型であるためです。
このような問題は、それぞれのIDに異なる型を定義することで解決できます。
UserId get_user_id(const std::string& user_name);
GameId get_game_id(const std::string& game_title);
PlayInfo get_play_info(UserId user_id, GameId game_id);
UserId game_id = get_game_id("Legend of Zelda");
GameId user_id = get_user_id("John");
PlayInfo info_1 = get_play_info(user_id, game_id); // OK
PlayInfo info_2 = get_play_info(game_id, user_id); // コンパイルエラー!
このような型は使用目的やドメインを区別するためのものであり、中身はintやstd::stringで十分であることが多くあります。
このような場面において、RustではNew Type Idiomと呼ばれる手法が使われます1。これは既存の型をラップするもので、新しい型を以下のように簡単に定義することができます。
// New Type Idiom in Rust
struct UserId(i64);
struct GameId(i64);
同様のことをC++でも行いたいというのが今回のテーマです。
ライブラリの紹介
New Typeを簡単に定義するためのライブラリを作りました。シングルヘッダーライブラリなので、ファイルをインクルードするだけで使えます。
コードはGitHubに上げています。
必要バージョン
C++ 17以上
インストール方法
new_type.hpp
をインクルードしてください。
使い方
既存の型Tを元に新しい型を定義するには以下のように宣言してください。
#include <new_type/new_type.hpp>
using MyType = newtype::NewType<T, Tag>;
新しい型を定義するにはユニークなタグ型が必要になります。このタグ型はクラスや構造体を使いますが、定義は必要ありません。前方宣言だけで十分です。
using UserId = newtype::NewType<int, struct UserIdTag>;
using GameId = newtype::NewType<int, struct GameIdTag>;
using FirstName = newtype::NewType<std::string, struct FirstNameTag>;
using LastName = newtype::NewType<std::string, struct LastNameTag>;
もしくは、以下のようにNEWTYPE_DEFINE_NEW_TYPE
マクロを使って型を定義します。
NEWTYPE_DEFINE_NEW_TYPE(UserId, int)
NEWTYPE_DEFINE_NEW_TYPE(GameId, int)
NEWTYPE_DEFINE_NEW_TYPE(FirstName, std::string)
NEWTYPE_DEFINE_NEW_TYPE(LastName, std::string)
インスタンスを作るには、元になった型の値をコンストラクタに渡してください。
MyInt value = MyInt{1}; // or MyInt(1)
中身の値を取り出すには間接参照演算子*
を使うか、get()
を呼んでください。
MyInt value = MyInt{1};
assert(*value == 1);
assert(value.get() == 1);
中身の型が持つメソッドを呼び出す場合は、アロー演算子->
を使います。
using FirstName = newtype::NewType<std::string, struct FirstNameTag>;
FirstName value = FirstName{"John"};
assert(value->size() == 4);
同じnew type型同士では以下のように比較できます。
static_assert(MyInt{1} != MyInt{2});
static_assert(MyInt{1} < MyInt{2});
new typeは、中身の型が同じであったとしても、他の型への変換はできません。
using MyInt1 = newtype::NewType<int, struct MyIntTag1>;
using MyInt2 = newtype::NewType<int, struct MyIntTag2>;
void func(MyInt1 x); // (A)
void func(MyInt2 x); // (B)
func(MyInt1{0}); // (A) が呼ばれる
func(MyInt2{0}); // (B) が呼ばれる
func(0); // コンパイルエラー
MyInt1 value = MyInt2{0}; // コンパイルエラー
中身の型が対応していれば、new typeをconstexprの文脈で使うことができます。
constexpr MyInt x = MyInt{10};
テストしたコンパイラ
- gcc 11.4.0
ライセンス
MIT License
-
RustのNew Typeは振る舞いの追加など、バリデーション以外の使い方もありますが、この記事では省きます。 ↩