🥕
C++でRustっぽいenumを実現する
Rustのenum
RustのenumはC++のenumと違い,値を格納できます.
enum class MyEnum
{
A,
B,
C
};
struct Inner {
a: usize,
b: Vec<i32>,
}
enum Hoge {
A,
B(i32, String),
C(Inner),
D { c: i32, d: String },
}
C++の場合,std::variant
ならrustのenumと同様値を格納できますが,内部に格納された値の型を間違えると実行時エラーになります.
int main()
{
std::variant<int, std::string> hoge2 = 1;
// 実行時エラー
std::cout << std::get<std::string>(hoge2) << std::endl;
}
また,同じ型を持つ項目があると少し面倒になります.(コードは省略)
それに対してRustの場合は,パターンマッチの漏れがないか,内部型をチェックせずにアクセスしていないか,といったことをコンパイル時点でチェックする機能もあります.
fn main() {
let hoge1 = Hoge::A;
let hoge2 = Hoge::B(1);
match hoge1 {
Hoge::A => println!("hoge1 is A"),
Hoge::B(x) => println!("hoge1 is B: {}", x),
Hoge::C(x) => println!("hoge1 is C: {}", x),
Hoge::D(inner, s) => {
println!("hoge1 is D: {} {}", inner.a, s);
for i in inner.b {
println!("inner.b: {}", i);
}
}
Hoge::E { c, d } => {
println!("hoge1 is E: {} {}", c, d);
} // Hoge::E の分岐がないのでコンパイルエラーになる
}
// 内部の値にアクセスする前に分岐で取り出す必要がある
if let Hoge::B(x) = hoge2 {
// Bの場合の処理
println!("hoge2 is B: {}", x);
} else {
// B以外だった場合の処理をelseで書くこともできる
println!("hoge2 is not B");
}
// 分岐を使わずに直接アクセスしようとするとコンパイルエラーになる
let Hoge::B(x) = hoge2; // コンパイルエラー
}
C++でも次のように記述すると,同様の機能を実装できます.
値を格納できるようにする
これは単純に,variantの内部に格納する専用の型を用意すれば良いです.
構造体の名前で区別することで,同じ値の項目もわかりやすく区別できます.
#include <variant>
#include <string>
#include <vector>
#include <iostream>
struct A
{
};
struct B
{
int inner;
};
struct C
{
int inner;
};
struct D
{
std::vector<int> a;
std::string b;
};
using Hoge = std::variant<A, B, C, D>;
int main()
{
Hoge hoge1 = A{};
Hoge hoge2 = B{1};
Hoge hoge3 = C{2};
std::cout << "hoge2: " << std::get<B>(hoge2).inner << std::endl;
std::cout << "hoge3: " << std::get<C>(hoge3).inner << std::endl;
return 0;
}
分岐の実装漏れを防ぐ
こちらが本題,かつ少々複雑です.
最初に結論を示すと,次のようになります.
#include <variant>
#include <string>
#include <vector>
#include <iostream>
template <typename...>
constexpr bool false_v = false;
struct Fugo
{
int a;
std::vector<char> b;
};
using Hoge = std::variant<int, std::string, size_t, Fugo>;
int main()
{
Hoge hoge;
std::visit(
[](auto &&arg)
{
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>)
{
std::cout << "int: " << arg << std::endl;
}
else if constexpr (std::is_same_v<T, std::string>)
{
std::cout << "string: " << arg << std::endl;
}
else if constexpr (std::is_same_v<T, size_t>)
{
std::cout << "size_t: " << arg << std::endl;
}
// 以下をコメントアウトするとコンパイルエラーになる
// -----ここから-----
else if constexpr (std::is_same_v<T, Fugo>)
{
std::cout << "Fugo: " << arg.a << std::endl;
for (const auto &c : arg.b)
{
std::cout << c;
}
std::cout << std::endl;
}
// -----ここまで-----
else {
static_assert(false_v<T>, "non-exhaustive visitor!");
}
},
hoge);
return 0;
}
std::visitの中身は「ジェネリックラムダ」で,引数の型で静的ディスパッチされます.
if constexpr文をstd::decay_t<decltype<arg>>とstd::is_same_vで判定することで,型による分岐をコンパイル時に判定できます.
また,最後のelse節では,どのif文でも分岐しなかった場合をstatic_assertでコンパイル時エラーにすることができます.
このときの注意点として,通常static_assertの判定がジェネリックラムダの型分岐より先なので,false_vというジェネリック型で型引数Tを受け取ることにより,型分岐が判定されるまでfalse_vの判定を遅延させる必要があります.
最後に
自分でもあまり理解できていませんが,とりあえず実装できたので良しとします.
ボイラープレートが多すぎて面倒なのは致し方ない……
Discussion