チュートリアル:C++ での強力な/不透明な typedef のエミュレート

先週、type_safe ライブラリをリリースしました。対応するブログ投稿でその機能について説明しましたが、ブログ投稿がかなり長くなったため、強力な typedef という 1 つの機能を取り上げることができませんでした。

型システムのエラーを防止したい場合、ストロングまたは不透明な typedef は非常に強力な機能です。オーバーロードなどを入力して許可し、暗黙的な変換を防止します。

残念なことに、C++ はそれらを作成するためのネイティブな方法を提供していないため、ライブラリ ベースのエミュレーションに頼る必要があります。

モチベーション

コードでいくつかのユニットを処理する必要があるとします。これで、優れた std::chrono と同じ手法を使用できます。 ライブラリですが、メートルとキログラムだけが必要な場合は、やり過ぎかもしれません。どの変数にどの単位が格納されているかをより明確にするために、いくつかの型エイリアスを定義します。

using meter = int;
using kilogram = int;

身長を int height と宣言する代わりに 、あなたは meter height と書きます .体格指数を計算する関数を書きたいと思うまでは、すべて素晴らしいです:

int bmi(meter height, kilogram weight);

時間が経ち、締め切りが近づき、深夜にその関数をどこかですぐに呼び出す必要があります:

auto result = bmi(w, h);

引数の正しい順序を忘れたり、関数を間違って呼び出したりして、デバッグに多くの時間を浪費しています。

さて、明らかに メートルはキログラムではないため、それらをキログラムに変換するとエラーになるはずです。しかし、コンパイラはそれを認識しません。型エイリアスは、同じの別の名前です。 type.Strong typedef はここで役立ちます:新しい 元の型と同じプロパティを持つ型.ただし、ある強力な typedef 型から別の型への暗黙的な変換はありません.

書いてみましょう。

すべてを手動で行う

もちろん、強力な typedef を非常に簡単に取得できます:ユーザー定義型を記述するだけです:

class meter
{
public:
 explicit meter(int val)
 : value_(val) {}

 explicit operator int() const noexcept
 {
 return value_;
 }

private:
 int value_;
};

新しいタイプ meter を作成しました 、明示的に int との間で変換可能です .int からの明示的な変換 次のようなエラーを防ぐのに役立ちます:

bmi(70, 180);

もう一度、パラメーターの順序を台無しにしましたが、新しい型が暗黙的に変換可能であれば問題なく動作します。int への明示的な変換 一方、できる これにより、次のことが可能になります:

void func(int);
…
func(meter(5));

しかし、意図を示すためにそこにキャストが必要な場合は、よりクリーンだと思います。 int への変換を行う ただし、明示的に指定すると、他の多くのことも防止できます。

auto m1 = meter(4);
m1 += 3; // error
auto m2 = m1 - meter(3); // error
if (m2 < m1) // error
 …

meter int ではありません であるため、何もできません。使用するすべての演算子をオーバーロードする必要があります。これはたくさんです。

幸いなことに、C++ は、少なくともその作業をライブラリに記述する方法を提供してくれます。

モジュラー ライブラリ

基本的な考え方は次のとおりです。いくつかの機能を実装する多くの「モジュール」を記述します。次に、必要なすべてのモジュールから継承する新しいクラス型を定義することにより、強力な typedef を記述できます。

基本モジュールは変換を定義し、値を保存します:

template <class Tag, typename T>
class strong_typedef
{
public:
 strong_typedef() : value_()
 {
 }

 explicit strong_typedef(const T& value) : value_(value)
 {
 }

 explicit strong_typedef(T&& value)
 noexcept(std::is_nothrow_move_constructible<T>::value)
 : value_(std::move(value))
 {
 }

 explicit operator T&() noexcept
 {
 return value_;
 }

 explicit operator const T&() const noexcept
 {
 return value_;
 }

 friend void swap(strong_typedef& a, strong_typedef& b) noexcept
 {
 using std::swap;
 swap(static_cast<T&>(a), static_cast<T&>(b));
 }

private:
 T value_;
};

基になる型と swap() の間の明示的な変換を提供します .Copy/move ctor/assignment は暗黙的であり、デフォルトのコンストラクターは値の初期化を行います。

Tag 強い typedef と強い型を区別するために使用されますが、それは単に新しい型そのものである可能性があります。

他のパブリック メンバーを提供しないため、インターフェイスが肥大化することはありません。また、基になる型からの割り当ても提供しません。

このモジュールで meter を作成できます 次のように入力します:

struct meter : strong_typedef<meter, int>
{
 // make constructors available
 using strong_typedef::strong_typedef;

 // overload required operators...
};

このモジュールは値の作成と格納を処理しますが、インターフェースを作成する必要があります。他のモジュールが登場する場所です。しかし、最初に、基礎となる型を取得する方法が必要です。それを手に入れる方法!

しかし、心配する必要はありません。非常に簡単に非メンバーにすることができます。最初のアプローチは、部分的なテンプレートの特殊化です:

template <typename T>
struct underlying_type_impl;

template <typename Tag, typename T>
struct underlying_type_impl<strong_typedef<Tag, T>>
{
 using type = T;
};

template <typename T>
using underlying_type = typename underlying_type_impl<T>::type;

部分的なテンプレートの特殊化を使用すると、型を分解してそのテンプレート引数を抽出できます。ただし、基本モジュールから継承して新しい強力な typedef を作成するため、このアプローチはここでは機能しません。underlying_type<meter> meter のため、形式が正しくありません strong_typedef から継承 クラス自体ではありません。そのため、派生から基底への変換を可能にする方法、つまり関数が必要です:

template <typename Tag, typename T>
T underlying_type_impl(strong_typedef<Tag, T>);

template <typename T>
using underlying_type
 = decltype(underlying_type_impl(std::declval<T>());

部分的な特殊化と同様に、テンプレート引数を取得できますが、今回は暗黙的な変換が可能です。

これで、強力な typedef の加算を実装するモジュールを作成できます:

template <class StrongTypedef>
struct addition
{
 friend StrongTypedef& operator+=(StrongTypedef& lhs,
 const StrongTypedef& rhs)
 {
 using type = underlying_type<StrongTypedef>;
 static_cast<type&>(lhs) += static_cast<const type&>(rhs);
 return lhs;
 }

 friend StrongTypedef operator+(const StrongTypedef& lhs,
 const StrongTypedef& rhs)
 {
 using type = underlying_type<StrongTypedef>;
 return StrongTypedef(static_cast<const type&>(lhs)
 + static_cast<const type&>(rhs));
 }
};

これは、いくつかの friend しか作成しない小さなクラスです。 問題は、強力な typedef 型に条件付きで演算子を提供したいということです。これを行うエレガントな方法は、これらの friend を使用することです。 関数.あなたが知らなかった場合、 friend を書くと class 内の関数定義 、関数名は外側の名前空間に挿入されず、ADL を介して検出されるだけです。

これで完璧です。friend を作成するだけです。 モジュール内の関数は、強力な typedef 型の演算子をオーバーロードします。モジュールから継承すると、派生クラスではフレンド関数を使用できますが、それ以外では使用できません。

モジュールでのアプローチは単純です:両方の引数を、演算子を提供する基本的な型に変換し、操作を実行して元に戻します。この戻り型の変換は非常に重要です。そうしないと、抽象化が失われてしまいます!

次に、モジュールを次のように使用できます。

struct meter
: strong_typedef<meter, int>, addition<meter>
{
 using strong_typedef::strong_typedef;
};

そして、次のコードはすでに整形式です:

meter a(4);
meter b(5);
b += meter(1);
meter c = a + b;

しかし、基になる型や他の型との追加が必要になるのでしょうか?単純に、mixed_addition<StrongTypedef, OtherType> を作成します。 モジュールからも継承します。

このアプローチにより、他のすべての一般的な演算子のオーバーロード用のモジュールを作成できます。マルチモジュールを作成することもできます:

template <class StrongTypedef>
struct integer_arithmetic : unary_plus<StrongTypedef>,
 unary_minus<StrongTypedef>,
 addition<StrongTypedef>,
 subtraction<StrongTypedef>,
 multiplication<StrongTypedef>,
 division<StrongTypedef>,
 modulo<StrongTypedef>,
 increment<StrongTypedef>,
 decrement<StrongTypedef>
{
};

では、すべてのオペレーターを直接オーバーロードしないのはなぜでしょうか?

しかし、なぜこのモジュール設計を使用しているのでしょうか? strong_typedef ですべてを提供しないのはなぜですか? 直接、継承全体を台無しにして、次のように記述します:

struct meter_tag {};

using meter = strong_typedef<meter_tag, int>;

ええと、型の安全性のためです。それが理由です。

組み込み型は非常に一般的です。これらは多くの操作を提供します。しかし、強力な typedef を作成する場合、多くの場合、その上に何らかのレベルのセマンティクスを追加します。また、意味をなさない操作もあります!

たとえば、OpenGL などの API で使用されるような整数ハンドルを扱っているとします。通常の整数を暗黙的にハンドルとして渡すことを防ぐために、強力な typedef を作成し、すべての演算子のオーバーロードが生成されると想像してください。

struct my_handle_tag {};

using my_handle = strong_typedef<my_handle_tag, unsigned>;

次のような無意味なコードを記述できるようになりました:

my_handle h;
++h; // increment a handle
h *= my_handle(5); // multiply a handle by 5
auto h2 = h / my_handle(2); // sure, divide by 2
…

要点はわかりました。

ハンドル型の場合、算術演算は必要ありません!必要なのは等価性とリレーショナル比較だけで、それ以上のことは必要ありません.

そのため、基本的な strong_typedef 私が説明したモジュールは何も作成しません オーバーロードが必要な場合は、モジュールから継承するか、演算子を自分でオーバーロードしてください。

ユーザー定義型はどうですか?

さて、これで、すべての一般的な演算子のオーバーロードのオーバーロードが作成され、整数や反復子に対しても強力な typedef を作成できるようになりました:

struct my_random_access_iterator
: strong_typedef<my_random_access_iterator, int*>,
 random_access_iterator<my_random_access_iterator, int>
{};

しかし、一部のタイプのインターフェースは、演算子だけで構成されていません (引用が必要です)。正確には:user-defined 型には、名前付きメンバー関数もあります。

ここで、強力な typedef エミュレーションが失敗します。演算子には (合理的な) セマンティクスと適切に定義されたインターフェイスがありますが、任意のメンバー関数にはありません。

したがって、(通常は) 汎用モジュールを作成することはできません。

struct my_new_udt
: strong_typedef<my_new_udt, udt>
{
 void foo(my_new_udt& u)
 {
 static_cast<udt&>(*this).foo(static_cast<udt&>(u));
 }

 my_new_udt bar(int i) const
 {
 return my_new_udt(static_cast<const udt&>(*this).bar(i));
 }

 my_new_udt& foobar()
 {
 auto& udt = static_cast<udt&>(*this).foobar();
 // Uhm, how am I supposed to convert it to exactly?
 }
};

これは冗長です .その問題にも真の解決策はありません。

ある operator.() 基礎となる型を認識せずに関数を呼び出すことを許可する提案ですが、引数や戻り値の型を基礎ではなく強力な typedef 型に変換しません。

これがまさに理由です この種の作業を自動的に行うには、言語機能として強力な typedef が必要であるか、少なくともなんらかの形式のリフレクションが必要です。公平を期すために、状況は そう ではありません 多くの場合、組み込み型に強力な typedef が必要であり、および/または Tag のようなファントム型を追加できるため、悪いです。 strong_typedef で使用 ここでは、他の点では同一のタイプを区別します。

しかし、それができない状況では、うんざりしてしまいます。

結論

強力な typedef は、型にセマンティクスを追加し、コンパイル時にさらに多くのエラーをキャッチするための優れた方法です。しかし、C++ にはそれを作成するネイティブな方法がないため、C++ ではめったに使用されません。ユーザー定義型にそれらを使用するのは非常に冗長であるため、言語にはそれらのネイティブ サポートが本当に必要です。

ここに示されている強力な typedef 機能は、type_safe によって提供されます。すでに多くのモジュールを作成しました。それらは、サブ名前空間 strong_typedef_op で利用できます。 .まだお持ちでない場合は、このライブラリの他の機能の概要を説明した以前の投稿もご覧ください。