概要
typedef
でクラスにエイリアスを付けられる。
typedef int foo;
using
を用いたエイリアス宣言もある。
typedef
では分かり辛い関数型等もusing
を使うと人間に優しい。
using bar = int;
異なるtypedef
エイリアス名は型システム上は同じ型だと認識される。
using foo = int;
using bar = int;
static_assert(std::is_same<foo, bar>::value, "同じ型だよ");
数値型に、コンテクストに合わせてエイリアス名を付けるのは良く有る。
このイディオムはユーザーにとって危険な振舞いがまれに良く起きる。
例えば、異なるエイリアス名でも同じ型なので、互いに代入可能。
using inch_type = int;
using mm_type = int;
inch_type l;
mm_type h;
l = h; // あっ
ドキュメントに頼ってこれらを管理するのは大変危険。
エイリアスで異なる型を導入してコンパイラに検知させたい
==> strong typedef, opaque alias
実装
超簡単
# include <type_traits>
namespace internal {
template <typename T, typename Tag, bool IsClass>
struct opaque_alias_impl : T
{
private:
using self = opaque_alias<T, Tag, true>;
public:
using prototype = T;
using T::T // derive ctor
opaque_alias_impl () = default;
opaque_alias_impl (self const &) = default;
opaque_alias_impl (self &&) = default;
self & operator = (self const &) = default;
self & operator = (self &&) = default;
opaque_alias (T const & value) : T(value) {}
opaque_alias (T && value) : T(std::move(value)) {}
T & cast () { return *static_cast<T *>(this); }
T const & cast () const { return *static_cast<T const *>(this); }
};
template <typename T, typename Tag>
struct opaque_alias_impl<T, Tag, false>
{
private:
using self = opaque_alias<T, Tag, false>;
T value;
public:
using prototype = T;
opaque_alias_impl () = default;
opaque_alias_impl (self const &) = default;
opaque_alias_impl (self &&) = default;
self & operator = (self const &) = default;
self & operator = (self &&) = default;
opaque_alias (T const & value) : value(value) {}
opaque_alias (T && value) : value(std::move(value)) {}
T & cast () { return value; }
T const & cast () const { return value; }
};
} // internal
template <typename T, typename Tag>
using opaque_alias = internal::opaque_alias<T, Tag, std::is_class<T>::value>;
使用例
opaque_alias
クラステンプレートを使うと、異なるエイリアスを付けると同時に、異なる型を導入できる。
元の型 => エイリアス型は、暗黙に変換可能。
エイリアス型 => 元の型は、明示的にのみ変換可能。
新たに導入された型同士は、変換不可能。
using inch_type = opaque_alias<int, class inch_tag>;
using mm_type = opaque_alias<int, class mm_tag>;
inch_type l; // OK
mm_type h(100); // OK
l = 0.5; // OK
h = 3.0; // OK
l = h; // NG
l = l.cast(); // OK
int i;
i = l // NG;
i = l.cast() // OK;
クラスのエイリアス型を作ると、、元のクラスのメンバに直接アクセスできる。
struct foo
{
int f () { return 1; }
};
using foo_alias_type = opaque_alias<foo, class foo_tag>;
foo_alias_type a;
a.f();
エイリアス型 => 元の型を暗黙に変換可能にすると、意図しない動作が可能になってしまう。
もし、それでも良いのなら、型変換演算子を定義すれば良い。
ちなみに、型変換演算子にexplicit
を付けると、(int &)(l)
等と書かないといけないため煩わしい。
参照を返さないと、元の型のコンストラクタが走る。
//implicit版
using inch_type = opaque_alias<int, class inch_tag>;
using mm_type = opaque_alias<int, class mm_tag>;
inch_type l;
mm_type h;
l = 1 + h; // h が int型に変換されて、(1 + h)の値がinch_typeに変換されてしまう。
エイリアスされた型は元の型とはもはや違うので、メタプログラミングで変な動作を起こすかも知れない。
簡易版なのでこんな物でいいでしょう。
strong typedef, opaque aliasに対して、柔軟に型変換の指定のできる文法をコア言語に追加する話があるらしいが、やりすぎだと思う。
エイリアス型=>元の型の暗黙の変換を許す場合、opaque_alias
を置いた名前空間がADLで捕捉されるので注意して下さい。