C++でNew Type Idiom

2024年2月10日 13:58 追記: `NEWTYPE_DEFINE_NEW_TYPE`マクロを使って型を定義する方法を追加しました。

---

New Type Idiomと呼ばれるRustの手法の紹介と、同様のことをC++で簡単に行うための自作ライブラリの紹介をしたいと思います。

# 動機

intやstd::stringのような型は汎用的であるために、他の値と混同しやすい問題があります。例えば以下のようなコードがあるとします。

```C++
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に異なる型を定義することで解決できます。

```C++
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]。これは既存の型をラップするもので、新しい型を以下のように簡単に定義することができます。

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

```Rust
// New Type Idiom in Rust
struct UserId(i64);
struct GameId(i64);
```

同様のことをC++でも行いたいというのが今回のテーマです。

# ライブラリの紹介

New Typeを簡単に定義するためのライブラリを作りました。シングルヘッダーライブラリなので、ファイルをインクルードするだけで使えます。

コードはGitHubに上げています。

https://github.com/Yosh31207/new_type_for_cpp

# 必要バージョン

C++ 17以上

# インストール方法

`new_type.hpp` をインクルードしてください。

## 使い方

既存の型Tを元に新しい型を定義するには以下のように宣言してください。

```C++
#include <new_type/new_type.hpp>

using MyType = newtype::NewType<T, Tag>;
```

新しい型を定義するにはユニークなタグ型が必要になります。このタグ型はクラスや構造体を使いますが、定義は必要ありません。前方宣言だけで十分です。

```C++
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`マクロを使って型を定義します。

```C++
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)
```

インスタンスを作るには、元になった型の値をコンストラクタに渡してください。

```C++
MyInt value = MyInt{1};   // or MyInt(1)
```

中身の値を取り出すには間接参照演算子``*``を使うか、``get()``を呼んでください。

```C++
MyInt value = MyInt{1};

assert(*value == 1);
assert(value.get() == 1);
```

中身の型が持つメソッドを呼び出す場合は、アロー演算子``->``を使います。


```C++
using FirstName = newtype::NewType<std::string, struct FirstNameTag>;

FirstName value = FirstName{"John"};
assert(value->size() == 4);
```

同じnew type型同士では以下のように比較できます。

```C++
static_assert(MyInt{1} != MyInt{2});
static_assert(MyInt{1} <  MyInt{2});
```

new typeは、中身の型が同じであったとしても、他の型への変換はできません。

```C++
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の文脈で使うことができます。

```C++
constexpr MyInt x = MyInt{10};
```

## テストしたコンパイラ

* gcc 11.4.0

## ライセンス

MIT License