表題の件、意外に難しい。Cの頃からunionと言えば…
#include <iostream>
#include <iomanip>
using namespace std;
union hoge {
uint64_t u64;
uint8_t u8[8];
};
int main() {
hoge o;
o.u64 = 1234;
for (auto b: o.u8) {
cout << hex << setw(2) << setfill('0') << static_cast<uint16_t>(b) << dec << " ";
}
cout << endl;
return 0;
}
// d2 04 00 00 00 00 00 00
こんな風にメモリ上の配置を見るのに使ったりするくらいで、あまり出番がないと思っていました。しかし、C++23で追加になったstd::expectedが今回rustのResultもどきらしく、それをC++11で簡易実装しようとしたらunionが必要になったのです。
で、結構ハマりました。
std::expectedとは
ようは戻り値で結果とエラーを同時に返すやつ。最近の言語には例外がなく、人によってはnullと同様に親の仇のごとく敵視されていると思う。なので、結果とエラーを同時に扱う型が重宝され、Cみたいにチマチマ条件分岐したり、エラーとセットでストリーム処理したりする。その型が件のResultだったり、Optionだったり、std::expectedだったりするわけです。
なぜunion?
結果とエラーを同時に扱うわけですが、結果とエラーは同時に存在しません。別々にメモリ確保してもいいけど、それは勿体ないというわけでunionの出番となるわけです。
#include <iostream>
#include <string>
using namespace std;
template<typename R, typename Error>
struct result {
bool success;
union Value {
R r;
Error e;
} v;
operator bool() {return success;}
R operator *() {return v.r;}
Error error() {return v.e;}
};
result<double, string> idiv(int left, int right) {
result<double, string> r;
if (right == 0) {r.e = string("cannot divide by 0");}
else {r.r = static_cast<double>(left) / right;}
return r;
}
int main() {
auto r = idiv(1,3);
if (r) cout << *r << endl;
else cout << r.error() << endl;
return 0;
}
最初に作ったのはこんなコードで、こんな感じにunionを使いたかったわけです。しかしこれはエラーになります。
unionはデフォルトコンストラクタが自動作成されない
見出しのとおりです。これのためにエラーになるわけです。
hoge.cpp: In function ‘result<double, std::__cxx11::basic_string<char> > idiv(int, int)’:
hoge.cpp:16:28: error: use of deleted function ‘result<double, std::__cxx11::basic_string<char> >::result()’
16 | result<double, string> r;
| ^
hoge.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つしか呼んではいけないのだから。
unionにコンストラクタを作る
なので、まずはunionにコンストラクタを作り、それを呼び出すためのコンストラクタをresult側にも用意します。
#include <iostream>
#include <string>
using namespace std;
template<typename R, typename Error>
struct result {
bool success;
union Value {
R r;
Error e;
Value(const R& r): r(r) {} // 追加
Value(const Error& e): e(e) {} // 追加
} v;
result(const R& r): success(true), v(r) {} // 追加
result(const Error& e): success(false), v(e) {} // 追加
operator bool() {return success;}
R operator *() {return v.r;}
Error error() {return v.e;}
};
result<double, string> idiv(int left, int right) {
if (right == 0) {return string("cannot divide by 0");} // 変更
else {return static_cast<double>(left) / right;} // 変更
}
int main() {
auto r = idiv(1,3);
if (r) cout << *r << endl;
else cout << r.error() << endl;
return 0;
}
これで動くかなと思いきや、まだエラーが出ます。
hoge.cpp: In function ‘result<double, std::__cxx11::basic_string<char> > idiv(int, int)’:
hoge.cpp:20:56: error: use of deleted function ‘result<double, std::__cxx11::basic_string<char> >::~result()’
20 | if (right == 0) {return string("cannot divide by 0");} // 変更
| ^
hoge.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にもデフォルトのデストラクタがないわけです。
外側のデストラクタ定義と内側のデストラクタの直呼び出し
というわけで、union内にはハリボテの空デストラクタを用意し、resultのデストラクタでunion内のメンバデストラクタを直接呼び出します。union自身にはどのメンバで構築されてるかの情報がないので。
#include <iostream>
#include <string>
using namespace std;
template<typename R, typename Error>
struct result {
bool success;
union Value {
R r;
Error e;
Value(const R& r): r(r) {}
Value(const Error& e): e(e) {}
~Value() {} // 追加: 外から正しく破棄すること
} v;
result(const R& r): success(true), v(r) {}
result(const Error& e): success(false), v(e) {}
~result() { // 追加
if (success) v.r.~R(); // 追加
else v.e.~Error(); // 追加
} // 追加
operator bool() {return success;}
R operator *() {return v.r;}
Error error() {return v.e;}
};
result<double, string> idiv(int left, int right) {
if (right == 0) {return string("cannot divide by 0");}
else {return static_cast<double>(left) / right;}
}
int main() {
auto r = idiv(1,3);
if (r) cout << *r << endl;
else cout << r.error() << endl;
return 0;
}
しかしコンパイルするとこれでもエラーになります。
hoge.cpp: In function ‘result<double, std::__cxx11::basic_string<char> > idiv(int, int)’:
hoge.cpp:25:56: 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");}
| ^
hoge.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のためにコピーコンストラクタも自動では生成されないことが原因です。
コピーコンストラクタ定義とplacement new
コピーコンストラクタを定義するわけですが、このコピーが一筋縄では出来ません。オリジナルのメンバ変数によってunionのメンバイニシャライザ呼び出しを切り替えられないため、メンバイニシャライザを使えないからです。なので以下のようにします。
#include <iostream>
#include <string>
using namespace std;
template<typename R, typename Error>
struct result {
bool success;
union Value {
R r;
Error e;
Value(){} // 追加
Value(const R& r): r(r) {}
Value(const Error& e): e(e) {}
~Value() {}
} v;
result(const R& r): success(true), v(r) {}
result(const Error& e): success(false), v(e) {}
result(const result& org): success(org.success) { // 追加
if (success) new (&v.r) R(org.v.r); // 追加
else new (&v.e) Error(org.v.e); // 追加
} // 追加
~result() {
if (success) v.r.~R();
else v.e.~Error();
}
operator bool() {return success;}
R operator *() {return v.r;}
Error error() {return v.e;}
};
result<double, string> idiv(int left, int right) {
if (right == 0) {return string("cannot divide by 0");}
else {return static_cast<double>(left) / right;}
}
int main() {
auto r = idiv(1,3);
if (r) cout << *r << endl;
else cout << r.error() << endl;
return 0;
}
ついにunionに空のデフォルトコンストラクタが入っています。これはメンバイニシャライザが使えないので、どうしても必要になるものです。デフォルトデストラクタもそうなのですが、両方必要悪なのです。
これはようやく動いて
0.333333
となります。めでたしめでたし。
まとめ
class/structにprimitiveでないunionメンバを入れるとデフォルトコンストラクタなどが消えて結構大変なので注意!
参考リンク