LoginSignup
3
0

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

  1. RustのNew Typeは振る舞いの追加など、バリデーション以外の使い方もありますが、この記事では省きます。

3
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0