概要
Rustではenum(正確にはその内のC言語ライクな列挙型)から整数型(usizeやi32など)にキャストすることができます。
enum Color {
Red,
Green,
Blue,
}
fn main() {
let v = vec![1, 2, 3];
println!("{}", v[Color::Red as usize]); // 1
}
しかし整数型からenumにはキャストできません。
let c = 1 as Color; // error[E0605]: non-primitive cast: `i32` as `Color`
こうなるのはRustのenumでは範囲外の数値をenumとして保持することを許していないためだと考えられます。
そこでこの記事では整数型とenumを行き来できる、もしくはそれと同等なことが可能なやり方として以下の3つを紹介します。
- enumとFromPrimitiveを使う(
let c: Color = num::FromPrimitive::from_usize(n).unwrap();
) - タプル構造体を使う(
struct Color(usize);
) - エイリアスを使う(
type Color = usize;
)
結論を先に書いてしまうと、
条件分岐の実行時オーバーヘッドと外部クレートの使用を許容できるなら安全性の高い1のやり方を、
コンパイル時に整数型と区別したかったりメソッドの追加が必要なら2のやり方を、
単なるusizeの別名で良ければ3のやり方を採用するのが良いと思います。
以下ではこのことを具体的に説明していきます。
そもそも整数型からenumに変換したい状況とは
整数型からenumに変換したい状況として、たとえば整数の計算結果を途中でenumとしても活用したい場合が考えられます。
他には、たとえば32個の4色(2bit)の並びのパターンを64bit整数に詰め込んで大量にキャッシュとしてメモリに保存したい状況が考えられます。
この場合、以下のようにビット列は整数型を介して取得・保存し、取得した色はenumとして扱うと型の恩恵を受けられます。
- ビット列から特定の位置の2bitを取り出す(整数値)
- 整数値をenumに変換して何らかの処理を実行する
- enumを整数値に変換してビット列の特定の位置に詰め込む
そしてこのようなユースケースの多くはゲーム木探索のようなCPU処理の最適化が求められることが想定されます。
これらのユースケースでは、型の恩恵は受けたいが変換はゼロオーバーヘッドで行われて欲しいことになります。
C++のenumはまさにこのような要望を叶えてくれる仕様となっています。
# include <iostream>
# include <vector>
enum Color {
Red,
Green,
Blue
};
void print(Color c) {
std::cout << c << std::endl;
}
int main() {
std::vector<int> v = {1, 2, 3};
Color c = Color::Red;
std::cout << v[c] << std::endl; // vectorの添字としてそのまま使用可能
// error: invalid conversion from 'int' to 'Color' [-fpermissive]
// print(0); // enum型にキャストしないとコンパイルエラー
print(static_cast<Color>(1)); // キャストすればOK
print(static_cast<Color>(100)); // しかし範囲外の値もキャストできてしまう
std::cout << (c < Color::Green) << std::endl; // 1 (true)
std::cout << (c == Color::Red) << std::endl; // 1 (true)
// enumは++や+=が使用できないので、static_castでunsignedに変換してからenumに戻す必要あり
for (Color i = Color::Red; i <= Color::Blue; i = static_cast<Color>(static_cast<unsigned>(i) + 1)) {
std::cout << i << std::endl;
}
std::cout << sizeof(Color) << std::endl; // 4 (32 bits)
std::cout << (sizeof Color::Red) << std::endl; // 4 (32 bits)
}
ただ、C++のenumはコードコメントに記載した通り、範囲外の値であることのチェックはありません。
Rustのenumでは範囲外の値でないことのチェックが必須となる点がC++のenumと異なります。
以下では概要で述べた3つのやり方を順に説明します。
enumとFromPrimitiveを使う
Rustはデフォルトでは整数型からenumに変換する機能を提供していませんが、num-deriveという外部クレートを使うとFromPrimitiveという関数で整数型からenumに変換できるようになります。
ただし、数値からenumの値を取得するには範囲外の数値が与えられた場合を考える必要があるためFromPrimitiveの戻り値はOption型となっており、エラーハンドリングを書くか、unwrapを呼ぶ必要があります。
# [derive(FromPrimitive, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
# [repr(usize)] // これを指定すると各値がusizeに
pub enum ColorEnum {
Red,
Green,
Blue,
}
extern crate num;
# [macro_use]
extern crate num_derive;
mod cppenum;
use cppenum::ColorEnum;
fn dump_color_enum(c: ColorEnum) {
println!("{:?}", c); // Green (cが1のとき)
}
fn main() {
let v = vec![1, 2, 3];
let ce = ColorEnum::Red;
// error[E0277]: the type `[{integer}]` cannot be indexed by `ColorEnum`
// println!("{}", v[ce]); // enumは直接添字に使用できない
println!("{}", v[ce as usize]); // enum -> usizeへはasでキャスト可能
let n: usize = 1;
// FromPrimitiveを使用。引数から型推論されてOption<ColorEnum>が生成されるのでそれをunwrap
dump_color_enum(num::FromPrimitive::from_usize(n).unwrap());
println!("{:?}", ce < ColorEnum::Green); // true
println!("{:?}", ce == ColorEnum::Red); // true
// for文の範囲指定ではas usizeが必要。(もしくはColorEnumにIteratorを実装)
for c in ColorEnum::Red as usize..=ColorEnum::Blue as usize {
println!("{}", c as usize);
}
println!("size_of ColorEnum: {}", std::mem::size_of::<ColorEnum>()); // 8 (64 bits)
println!("size_of ColorEnum::Red: {}", std::mem::size_of_val(&ColorEnum::Red)); // 8 (64 bits)
}
外部クレートを使用したくない場合は、以下のようにmatchを使用して自分で実装することもできます。
impl Color {
fn from_usize(n: usize) -> Option<Color> {
match n {
0 => Some(Color::Red),
1 => Some(Color::Green),
2 => Some(Color::Blue),
_ => None
}
}
}
fn main() {
let c = Color::from_usize(1).unwrap(); // unwrapで値を取得。Noneが返ってくるとpanicが呼ばれる
println!("{}", c as usize); // 1
}
タプル構造体を使う
次はタプル構造体を使う場合です。C++のenumと同じ感覚で使うには最もお勧めの方法です。
具体的には、コンパイル時に整数型とは区別したいが、enumに範囲外の数値が与えられる可能性は気にしない場合です。
// 数値型のように比較やコピーができるようにこれらをderiveしておくと便利
# [derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Color(pub usize); // モジュール外からColor(1)のように初期化をするならpubの指定が必要
impl Color {
pub const RED: Self = Self(0);
pub const GREEN: Self = Self(1);
pub const BLUE: Self = Self(2);
}
fn dump_color(c: Color) {
println!("{:?}", c); // Color(1) (cが1のとき)
}
fn main() {
let v = vec![1, 2, 3];
let cs = Color::RED;
// error[E0277]: the type `[{integer}]` cannot be indexed by `Color`
// println!("{}", v[cs]); // structは直接添字に使用できない
println!("{}", v[cs.0]); // タプル構造体の1つ目のメンバは0でアクセス可能
let n: usize = 1;
dump_color(Color(n)); // タプル構造体の初期化
println!("{:?}", cs < Color::GREEN); // true
println!("{:?}", cs == Color::RED); // true
// for文の範囲指定では.0でusizeのメンバを取り出す(もしくはColorにIteratorを実装)
for c in Color::RED.0..=Color::BLUE.0 {
println!("{}", c);
}
println!("size_of Color: {}", std::mem::size_of::<Color>()); // 8 (64 bits)
println!("{}", cs); // Color::RED
}
タプル構造体のメンバには.0
でアクセスします。
メンバが1つしかないのにこの書き方をするのはちょっと気持ち悪いかもしれないですが、簡潔な書き方ではあります。
ちなみにprint時に数値をそのまま出力するのではなくて文字列にして出力したい場合、以下のDisplayトレイトを実装しておくと便利です。
impl std::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Color::RED => write!(f, "Color::RED"),
Color::GREEN => write!(f, "Color::GREEN"),
Color::BLUE => write!(f, "Color::BLUE"),
_ => write!(f, "{}", self.0),
}
}
}
fn main() {
let c = Color::RED;
println!("{}", c); // Color::RED
}
エイリアスを使う
最後に紹介するのは型の別名(エイリアス)を使うやり方です。
最も簡単ではあるもののusizeとコンパイル時に区別されないことからenumの代替として使う場合は注意が必要です。
pub type ColorType = usize;
pub const RED: ColorType = 0;
pub const GREEN: ColorType = 1;
pub const BLUE: ColorType = 2;
fn dump_color_type(c: ColorType) {
println!("{:?}", c); // 1 (cが1のとき)
}
fn main() {
let v = vec![1, 2, 3];
let ct = RED;
println!("{}", v[ct]); // Vecの添字に使用可能
let n: usize = 1;
dump_color_type(n); // usizeの変数をそのまま入れられる
println!("{:?}", ct < GREEN); // true
println!("{:?}", ct == RED); // true
for c in RED..=BLUE { // for文の範囲にも使用可能
println!("{}", c);
}
println!("size_of ColorType: {}", std::mem::size_of::<ColorType>()); // 8 (64 bits)
pub trait Foo { fn foo(&self) { println!("foo!"); } }
impl Foo for ColorType {}
RED.foo(); // ColorTypeにimplしたfoo関数の呼び出し
1.foo(); // ColorTypeにimplするとusizeにもimplしたことになる
}
最も数値として扱うことが簡単になりますが、usizeと区別されないため、コード中に書いた通りトレイトを実装するとusizeにも実装されてしまうことに注意が必要です。
ただ、別名をつけると見た目やIDE上での扱いで区別できる点は便利です。
なお、エイリアスの本来の用途は公式ドキュメントの"エイリアス"が分かりやすいです。
おわりに
Rustのenumでは範囲外の値のチェックが必須となりますが、タプル構造体を利用することでC++のenumのような範囲外の値を許容する列挙型(のようなもの)を再現できます。
Rustはデフォルトでは配列の境界値チェックも実行時に行いますし、若干のオーバーヘッドに対して大きな安全性を確保できる場合は安全側に倒す思想であることが伝わってきます。
その上でRustは多くの抜け道を用意してくれてますので、ここぞという時にその僅かなオーバーヘッドを削減できるところもまた魅力だと思います。