unionメンバを持つstruct(class)の扱い
序
この記事は以前Qiitaに掲載していたモノ(BANされたので今は削除されている)を少し手直ししたものです。
Zennに関連記事を書くかもしれないので引っ張ってきました。
表題の件、意外に難しい。Cの頃からunionと言えば…
こんな風にメモリ上のアラインメントを見るのに使ったりするくらいで、あまり出番がないと思っていました。しかし、C++23で追加になったstd::expectedが今回rustのResultもどきらしく、それをC++11で簡易実装しようとしたらunionが必要になったのです。
で、結構ハマりました。
1. std::expectedとは
ようは戻り値で結果とエラーを同時に返すやつです。最近の言語には例外がなく、人によってはnullと同様に親の仇のごとく敵視されていると思います。なので、結果とエラーを同時に扱う型が重宝され、Cみたいにチマチマ条件分岐したり、エラーとセットでストリーム処理したりします。その型がRustのResult/Optionだったり、std::expected(c++23)/std::optional(c++17)だったりするわけです。
2. なぜunionが必要なのか?
結果とエラーを同時に扱うわけですが、結果とエラーは同時に存在しません。別々にメモリ確保してもいいけど、それは勿体ないというわけでunionの出番となるわけです(C++17にはstd::variantがありますが、C++11にはありません)。
最初に作ったのは以下のコードで、こんな感じにunionを使いたかったわけです。
しかしこれはエラーになります。
3. なぜエラーになったのか?
unionはデフォルトコンストラクタが自動作成されないからです。
div.cpp: In function 'result<double, std::__cxx11::basic_string<char> > idiv(int, int)':
div.cpp:16:28: error: use of deleted function 'result<double, std::__cxx11::basic_string<char> >::result()'
16 | result<double, string> r;
| ^
div.cpp:5:8: note: 'result<double, std::__cxx11::basic_string<char> >::result()' is implicitly deleted because the default definition would be ill-formed:
5 | struct result {
| ^~~~~~
最初に書いたprimitiveなunionは良かったのですが、unionのメンバにコンストラクタを持つオブジェクトなどが入ってくると、デフォルトのコンストラクタはunion内のどのメンバのコンストラクタを呼ぶ必要があるのか分かりません。どれか1つしか呼んではいけないのだから。
4. unionにコンストラクタを作る
なので、まずはunionにコンストラクタを作り、それを呼び出すためのコンストラクタをresult側にも用意します。
これで動くかなと思いきや、まだエラーが出ます。
div.cpp: In function 'result<double, std::__cxx11::basic_string<char> > idiv(int, int)':
div.cpp:20:29: error: use of deleted function 'result<double, std::__cxx11::basic_string<char> >::~result()'
20 | if (right == 0) {return string("cannot divide by 0");} // 変更
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
div.cpp:5:8: note: 'result<double, std::__cxx11::basic_string<char> >::~result()' is implicitly deleted because the default definition would be ill-formed:
5 | struct result {
| ^~~~~~
今度はデフォルトのデストラクタがないと怒られています。そうなのです。unionにはデフォルトのデストラクタがないので、それをaggregateしているresultにもデフォルトのデストラクタがないわけです。
5. unionの外からデストラクタを呼ぶ
というわけで、union内にはハリボテの空デストラクタを用意し、resultのデストラクタでunion内のメンバデストラクタを直接呼び出します。union自身にはどのメンバで構築されてるかの情報がないので。
しかしコンパイルするとこれでもエラーになります。
div.cpp: In function 'result<double, std::__cxx11::basic_string<char> > idiv(int, int)':
div.cpp:25:29: error: use of deleted function 'result<double, std::__cxx11::basic_string<char> >::result(const result<double, std::__cxx11::basic_string<char> >&)'
25 | if (right == 0) {return string("cannot divide by 0");}
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
div.cpp:5:8: note: 'result<double, std::__cxx11::basic_string<char> >::result(const result<double, std::__cxx11::basic_string<char> >&)' is implicitly deleted because the default definition would be ill-formed:
5 | struct result {
| ^~~~~~
今度はresultのコピーコンストラクタがないと怒られています。戻り値で一時オブジェクトを構築するわけですが、それを呼び出し側の変数にコピーできないということです。これは例によってunionのためにコピーコンストラクタも自動では生成されないことが原因です。
6. コピーコンストラクタ定義とplacement new
コピーコンストラクタを定義するわけですが、このコピーが一筋縄では出来ません。オリジナルのメンバ変数によってunionのメンバイニシャライザ呼び出しを切り替えられないため、メンバイニシャライザを使えないからです。なので以下のようにします。
ついにunionに空のデフォルトコンストラクタが入っています。これはメンバイニシャライザが使えないので、どうしても必要になるものです。デフォルトデストラクタもそうなのですが、両方必要悪なのです。
これはようやく動いて
0.333333
となります。めでたしめでたし。
7. まとめ
class/structにprimitiveでないunionメンバを入れるとデフォルトコンストラクタなどが消えて結構大変!
-
unionにデフォルトコンストラクタがない
→unionに引数付きのコンストラクタを用意する -
unionにデフォルトデストラクタがない
→unionの外からunionのデストラクタを直に呼び出す -
unionにコピーコンストラクタがない
→unionの外のコピーコンストラクタからplacement new
参考リンク
Discussion