しばらくブログを書いていなかったが、実際のところペーパーワークに駆られて(現在進行形で駆られている)コードをほぼ書けていない。そのストレスで日曜日に何の関係もないコードをゴリゴリ書いていた。そこでタイトルにあるものを使って、ボンミスで時間を飛ばしてしまった。
(2017年5月18日)以下の記事中に登場する私のコードに誤りがあると思われるので、それに関する記事を書いた。
C++11でunionの制限がかなり解除された。以下の記事に詳しい。
で、これを使って以下のようなクラスを書くことにしたのだ。目的は、動的型付けなフォーマットのファイルをパースすることである。以前にTOMLのパーサを作ったが、少し不満な点があったので、今C++98でも使えるという制限を解除して書いたらどうなるかと思って試してみた。あのコードは過不足なく動くのだが、使っていくうちに少し遅いと感じたり、また少し不便だと思ったところがあるのと、パーサそのものの実装をもう少し綺麗にできないかと感じているので、記事にしあぐねている間に別の実装をやり始めてしかもそこでの経験を記事にしてしまった。
C++11が使えるようになったのなら、そして複数の型を持てるようなvalue
を定義するなら、union
の出番であろう。初期化にはplacement new
が、破棄するときには明示的デストラクタ呼び出しが必要だが、毎回の読み出しにポインタをたぐらなくてよい分高速化が見込めるのではないだろうか、と考えてのことだ(計測していないのでどうか知らない)。
というわけで簡略化したコード。
enum class value_t { integer, string, array, empty, };
まず、型を示すenum
を定義する。これは別にunion
を持つクラス内でやってもいいのだが、今回は外で定義する。キャストのtemplate parameter
として使おうと思ったからだ。
そして、ある型が何に対応するかを取得できる関数を作る。
template<typename T> constexpr value_t check_type() { return std::is_integral<typename std::decay<T>::type>::value ? value_t::integer : std::is_same<typename std::decay<T>::type, std::string>::value ? value_t::string: std::is_same<typename std::decay<T>::type, std::vector<value>>::value ? value_t::array: value_t::empty; } template<typename T> struct value_traits { constexpr static value_t type_index = check_type<T>(); constexpr static bool is_value_type = type_index != value_t::empty; };
これ別に関数じゃなくてもトレイト内に直接作れる気が今してきた
で、本体だが、無名union
と型のenum
を持つことにする。インターフェースとしては、デフォルトctor/dtorと、コピー・ムーブctor、コピー・ムーブ演算子、加えて値を直接渡せるコンストラクタ、v.cast<value_t::Integer>()
のようにして参照を取り出せるアクセサ、別の型でも再代入可能な代入演算子あたりがほしい。
value::cast()
はテンプレート引数によって返す型が異なるので、その変換のためのヘルパーが必要だ。これは単に構造体を特殊化していくと作れる。type generator
的なものだ。
template<value_t> struct default_value_type{}; template<> struct default_value_type<value_t::integer> { typedef int type; } template<> struct default_value_type<value_t::string> { typedef std::string type; } template<> struct default_value_type<value_t::array> { typedef std::vector<value> type; }
で、本体の宣言。
class value { public: value(); ~value(); value(value const&); value(value&&); value& operator=(value const&); value& operator=(value&&); template<typename T, std::enable_if< value_traits<T>::is_value_type, std::nullptr_t>::type = nullptr> value(T&& v); template<typename T, std::enable_if< value_traits<T>::is_value_type, std::nullptr_t>::type = nullptr> value& operator=(T&& v); value_t type() const {return type_;} template<value_t type_index> typename default_value_type<type_index>::type& cast(); template<value_t type_index> typename default_value_type<type_index>::type const& cast() const; private: void switch_clean(); template<value_t type_index, typename T> void switch_assign(T&& val); template<value_t> struct switch_cast; private: value_t type_; union { int integer_; std::string string_; std::vector<value> array_; }; };
さっき指定していない関数が3つある。その中ではswitch_cast
を作った理由が一番簡単だ。これはcast
関数が返す型をswitch
できるようにしている。実装は、
template<> struct value::switch_cast<value_t::Integer> { static int & invoke(value& v) {return v.integer_;} static int const& invoke(value const& v) {return v.integer_;} }; template<> struct value::switch_cast<value_t::String> { static std::string& invoke(value& v) {return v.string_;} static std::string const& invoke(value const& v) {return v.string_;} }; template<> struct value::switch_cast<value_t::Array> { static std::vector<value>& invoke(value& v) {return v.array_;} static std::vector<value> cosnt& invoke(value const& v) {return v.array_;} };
実装だが、union
を使うときには注意点がある。switch_clean
とswitch_assign
を作った理由はそれだ。その注意点とは、自明でない特殊メンバ関数(ctor/dtorなど)を持つ(非静的)メンバがunion
にあると、特殊メンバ関数が暗黙定義されなくなるのだ。なので、これは定義してやる必要がある。さらに、オブジェクトはplacement new
で構築する必要があり、またunion
を破棄する前に明示的にdtor
を呼んでやらねばならない。というのも、union
からしてみれば何が入っているかも知らないのに自動で正しいデストラクタを呼べというのは無茶ぶりであるので、こちらが管理してやらねばならないというのは頷ける。
というわけで以下のような実装になる。
inline void value::switch_clean() { switch(this->type_) { case type::integer : return; case type::string : {this->string_.~basic_string<char>(); return;} case type::array : {this->array_.~vector<value>(); return;} case type::empty : return; default : assert(false); } } template<value_t type_index, typename T> void value::switch_assign(T&& val) { switch(type_index) { case type::integer : integer_ = static_cast<int>(val); case type::string : {new(&(this->string_)) std::string(val); return;} case type::array : {new(&(this->array_)) std::vector<value>(val); return;} case type::empty : return; default : assert(false); } }
なんのことはない、placement new
とdtor
呼び出しをくくりだしているだけだ。ここで、switch_assign
のtype_index
はテンプレート引数なのだからstruct
の部分特殊化による分岐でコンパイル時に解決できるが、面倒なのでここではしない。また、まともなC++コンパイラは
template<std::size_t N> std::size_t f(){ std::size_t retval=0; for(std::size_t i=0; i<N; ++i) retval+=i*i; return retval; }
のようなコードをコンパイル時にループアンロールできるという話があるのでこの程度のswitch
はコンパイル時に解決してくれないものだろうか(未確認)。これを見てみるのも面白いかもしれない。
さて、特殊メンバ関数の実装は、上記を使って
value::value(): type_(value_t::Empty){} value::~value(){switch_clean();}
ここまでは良いのだが、テンプレート引数を動的に決められない以上value.cast<value.type()>
などとは出来ず、(コピー・ムーブ)(コンストラクタ・代入演算子)は以下のようにせざるを得ない。
value::value(const value& rhs): type_(rhs.type()) { switch(rhs.type()) { case value_t::integer: { switch_assign<value_t::integer>(rhs.cast<value_t::integer>()); break; } case value_t::string: { switch_assign<value_t::string>(rhs.cast<value_t::string>()); break; } case value_t::array: { switch_assign<value_t::array>(rhs.cast<value_t::array>()); break; } case value_t::empty: break; default : assert(false); } } /* 残りの、ムーブコンストラクタ、コピー代入演算子、ムーブ代入演算子が続く…… */
面倒だ。
ところで、switch_assign
を作った理由は別にある。value_traits
を覚えているだろうか。これによって、
template<typename T, std::enable_if< value_traits<T>::is_value_type, std::nullptr_t>::type> value::value(T&& v) { switch_assign<value_traits<T>::type_index>(std::forward<T>(v)); } /* 代入演算子も同様、ただし既に値が入っていた場合、必要に応じてデストラクタを呼ぶ */
という風に書けるのである。この目的のためにswitch_assign
を作ったのであって、コピーコンストラクタなどはベタ書きに比べて特に書きやすくなるわけではない。
ので、最初コピーコンストラクタなどはベタ書きしており、そこでplacement new
を使わず直接代入して、SIGSEGV
を受け取っていた。それで半日潰した。
union
ではコピー・ムーブコンストラクタや代入演算子でもplacement new
を使わなければならない。
当然すぎるのだが、何故か引っかかってしまった。
あと、コンストラクタでは気にならないのだが、代入演算子の場合、相手のvalue_t
タグとこちらのそれが違っていた場合、switch_clean
を呼ぶ必要がある。これを忘れるとかなり厳しいことになる。
また、ムーブ時に相手のvalue::type_
をムーブすると、type_
で分岐するデストラクタの挙動がUBになる……と思ったが、実際どうだろう。標準ライブラリのオブジェクトはムーブ後、「有効だが未規定な値」、つまり残骸に代入すればまた使えるが、残骸の中を見た時には何が起きるかわからない、という状態になっていたはずだが、そういえばenum
、もっと言えばその下にあるchar
やint
のようなfundamental type
の場合どうなるんだったか。未規定になるなら、デストラクタは困るだろう。なので、ムーブコンストラクタ・ムーブ代入演算子はtype_
をムーブしない方がよいように思える。少し調べるべきではあるが。実際、move
が威力を発揮するのはどちらかというと基本型よりもstd::vector
のようなリソースへのポインタなどを持っているオブジェクトや、リソースが唯一のものであることを保証すべき(std::unique_ptr
やstd::thread
)場面なので、value::type_
はコピーでもさほどの問題はないと思われる。
で、最後がcast()
だ。これは、switch_cast
をそのまま使って、
template<value_t T> typename detail::default_value_type<T>::type const& value::cast() const { if(T != type_) throw type_error("different type!"); return switch_cast<T>::invoke(*this) } /* 非const版も同様 */
のようにすればよい。
動的に型を取り出せない(呼び出し側がswitch
とかで正しく分岐する必要がある)のは少し不便なので、他にmatch
文もどきが書けるような仕組みを作ろうか。オペレータチェイン(だったっけか)を上手く使えばできるような気がするので、後で試してみよう。