突然だが私はunionが好きだ。特にメモリ効率を重視するC++においては利用した方が良い場合が稀にある。しかし、C++の型情報を尊重するスタンスとは相容れない部分で苦労する事になる。そう、unionに何の型が配置されているのか分からないのだ。
という訳で、C++17で追加されるunionライクなオブジェクトに型安全に連絡できるというstd::variantが待ち遠しいので、自分の都合の良いようにC++11で実装する。但し、インターフェースの互換性は考慮せず、std::variantとは合わせない。また、ヒープ領域に依存したくないので、ストレージはオブジェクト内に用意する。
先ずは外部インターフェースを考える。variantは複数の型を持ち、それぞれの型でだけ初期化できるものとする。メンバ関数visitに関数オブジェクトを渡す事で内部の型に応じた値を引数として関数オブジェクトを呼び出す。以上の仕様から、以下のような型を定義する。
template <class... Types>
struct variant
{
variant() = default;
template <class T>
variant(T&& t);
template <class F, class... Args>
typename std::result_of<F(Args...)>::type visit(F&& f);
};
次に、ヒープ領域の代わりとして、Types...の型が要求する最大サイズ及び最大アラインメントを収納する為のストレージを定義する。ここで、Types...から必要な情報を取得する為にvariant_storageを定義する。なおストレージにはstd::aligned_storageを利用する。
template <class...>
struct variant_storage;
template <class A, class... B>
struct variant_storage<A, B...> : variant_storage<B...> {
using base_t = variant_storage<B...>;
static constexpr auto size = sizeof(A) > base_t::size ? sizeof(A) : base_t::size;
static constexpr auto align = alignof(A) > base_t::align ? alignof(A) : base_t::align;
};
template <class Z>
struct variant_storage<Z> {
static constexpr auto size = sizeof(Z);
static constexpr auto align = alignof(Z);
};
template <class... Types>
using storage_t = typename std::aligned_storage<variant_storage<Types...>::size, variant_storage<Types...>::align>::type;
};
次に、variant内部の型の識別子を保存する為のvariant_typeidを定義する。型の識別子はTypes....のインデックスをsize_tとして保存する。例えば、空の状態の識別子を0として、Types...の最後の型の識別子を1とする、最初の型の識別子はsizeof...(Types)である。また、variant_storageにインスタンスを配置する為の静的関数assignを定義する。assignはオブジェクトをストレージに転送し、その型の識別子を返す。
template <class...>
struct variant_typeid;
template <class A, class... B>
struct variant_typeid<A, B...> : variant_typeid<B...> {
using base_t = variant_typeid<B...>;
static constexpr size_t id = base_t::id + 1;
template <class T, typename std::enable_if<std::is_same<A, typename std::decay<T>::type>::value, std::nullptr_t>::type = nullptr>
static size_t assign(void* p, T&& t) {
::new(p) A(std::forward<T>(t));
return id;
}
using base_t::assign;
};
template <class Z>
struct variant_typeid<Z> {
static constexpr size_t id = 1;
template <class T, typename std::enable_if<std::is_same<Z, typename std::decay<T>::type>::value, std::nullptr_t>::type = nullptr>
static size_t assign(void* p, T&& t) {
::new(p) Z(std::forward<T>(t));
return id;
}
};
そして、ストレージと識別子をvariantに持たせる。この時、コンストラクタで受け取ったオブジェクトをassignでストレージに転送する。この時、オブジェクトはコピーまたはムーブされる。
ここで、variantがTypes...の型で初期化できる事を確認する。
template <class... Types>
struct variant
{
variant() = default;
template <class T>
variant(T&& t)
: id_(variant_typeid<Types...>::assign(&storage_, std::forward<T>(t))) {
}
template <class F, class... Args>
typename std::result_of<F(Args...)>::type visit(F&& f);
storage_t<Types...> storage_;
size_t id_ = 0;
};
int main() {
using var_t = variant<char, short, int, float>;
var_t a; // OK
var_t b = static_cast<short>(1); // OK short
var_t c = 1; // OK int
var_t d = 1.f; // OK float
var_t e = 1.0; // error double
}
最後に、型を受け取る為のvisitを定義する。visitは、関数オブジェクトを受け取り、ストレージを内部の型のポインタに変換して関数オブジェクトに渡す。ここで、variantが空である場合はnullptrを渡す事とする。
これを実現する為には識別子である整数値から元々の型を特定し、復元しなければならない。識別子は0以上の連続する整数値である為、配列の要素として利用するのが良い。よって、型消去されたポインタと関数オブジェクトを受け取って、型を復元してから関数オブジェクトに渡すという変換関数の関数ポインタを配列に格納しておき、識別子を利用して目的の変換関数を呼び出す。配列への展開は可変長テンプレートのパラメータパックの展開によって作成する。また、識別子の順番とTypes...の順番は逆順になっている為、添え字の算出に注意する。
template <class T, class F>
auto declfunc() -> void(*)(void*, F&&) {
return [](void* p, F&& f){ return std::forward<F>(f)(static_cast<T*>(p)); };
}
template <class... Types>
struct variant
{
variant() = default;
template <class T>
variant(T&& t)
: id_(variant_typeid<Types...>::assign(&storage_, std::forward<T>(t))) {
}
template <class F>
void visit(F&& f) {
if (!id_) {
return std::forward<F>(f)(nullptr);
}
static void(*ftbl[])(void*, F&&) = { declfunc<Types, F>()... };
ftbl[sizeof...(Types) - id_](&storage_, std::forward<F>(f));
}
storage_t<Types...> storage_;
size_t id_ = 0;
};
以上でvariantとしての最低限の機能を有した事になる。具体的には以下の様に関数オブジェクトであるVisitorクラスを定義し、そのオブジェクトをvisitメンバ関数に渡す。visitでは型の変換関数を経由してVisitorの関数呼び出し演算子で返還後のポインタを受け取る事ができる。
struct Visitor
{
void operator ()(std::nullptr_t) { std::cout << "empty" << std::endl;}
void operator ()(float*) { std::cout << "float" << std::endl;}
template <class T>
void operator ()(T* x) { std::cout << typeid(x).name() << std::endl; }
};
int main() {
using var_t = variant<char, short, int, float, double>;
var_t a;
a.visit(Visitor()); // empty
a = static_cast<short>(1);
a.visit(Visitor()); // Ps
a = 1;
a.visit(Visitor()); // Pi
a = 1.f;
a.visit(Visitor()); // float
a = 1.0;
a.visit(Visitor()); // Pd
}
ところで、variantのデストラクタではストレージのオブジェクトが破棄されない。これを破棄する為にvisitを利用するとよい。この手法は様々なインターフェースに応用できる。
struct variant
{
struct deleter {
template <class T>
void operator ()(T* p) { p->~T(); }
void operator ()(std::nullptr_t) {}
};
~variant() {
visit(deleter());
}
//(以下略)
};
余談だが、C++14ではジェネリックラムダによって引数にautoが使用できる為、visitに渡す関数オブジェクトを1つのラムダ式で記述できるようになる。これにより、様々な型の組み合わせによって発生する、複雑なマルチディスパッチを簡単に実現する事ができる。これは中々に強力である。
int main() {
auto visitor = [](auto p){ std::cout << typeid(p).name() << std::endl; };
using var_t = variant<char, short, int, float, double>;
var_t a{ 1 };
a.visit(visitor); // Pi
var_t b{ 1.f };
b.visit(visitor); // Pf
a = 'A';
b = 0.0;
a.visit([&b](auto a){
b.visit([&a](auto b){
std::cout << typeid(a).name() << ", " << typeid(b).name() << std::endl; // Pc, Pd
});
});
}
最後に、今回定義したvariantのコードを示す。
コピーコンストラクタ等、端折っている部分があるので、このまま利用する事は非推奨である。
template <class...>
struct variant_storage;
template <class A, class... B>
struct variant_storage<A, B...> : variant_storage<B...> {
using base_t = variant_storage<B...>;
static constexpr auto size = sizeof(A) > base_t::size ? sizeof(A) : base_t::size;
static constexpr auto align = alignof(A) > base_t::align ? alignof(A) : base_t::align;
};
template <class Z>
struct variant_storage<Z> {
static constexpr auto size = sizeof(Z);
static constexpr auto align = alignof(Z);
};
template <class... Types>
using storage_t = typename std::aligned_storage<variant_storage<Types...>::size, variant_storage<Types...>::align>::type;
template <class...>
struct variant_typeid;
template <class A, class... B>
struct variant_typeid<A, B...> : variant_typeid<B...> {
using base_t = variant_typeid<B...>;
static constexpr size_t id = base_t::id + 1;
template <class T, typename std::enable_if<std::is_same<A, typename std::decay<T>::type>::value, std::nullptr_t>::type = nullptr>
static size_t assign(void* p, T&& t) {
::new(p) A(std::forward<T>(t));
return id;
}
using base_t::assign;
};
template <class Z>
struct variant_typeid<Z> {
static constexpr size_t id = 1;
template <class T, typename std::enable_if<std::is_same<Z, typename std::decay<T>::type>::value, std::nullptr_t>::type = nullptr>
static size_t assign(void* p, T&& t) {
::new(p) Z(std::forward<T>(t));
return id;
}
};
template <class T, class F>
auto declfunc() -> void(*)(void*, F&&) {
return [](void* p, F&& f){ return std::forward<F>(f)(static_cast<T*>(p)); };
}
template <class... Types>
struct variant
{
struct deleter {
template <class T>
void operator ()(T* p) { p->~T(); }
void operator ()(std::nullptr_t) {}
};
~variant() {
visit(deleter());
}
variant() = default;
template <class T>
variant(T&& t)
: id_(variant_typeid<Types...>::assign(&storage_, std::forward<T>(t))) {
}
template <class F>
void visit(F&& f) {
if (!id_) {
return std::forward<F>(f)(nullptr);
}
static void(*ftbl[])(void*, F&&) = { declfunc<Types, F>()... };
ftbl[sizeof...(Types) - id_](&storage_, std::forward<F>(f));
}
storage_t<Types...> storage_;
size_t id_ = 0;
};
以上