コンテンツにスキップ

Rustの高度なジェネリクス:constジェネリクス・const fn・PhantomData

ジェネリクスのページでは、ジェネリックなパラメータはいつも型でした。Pair<T> には i32 も &str も入れられました。このページでは、同じ考え方をもう一歩先へ進める3つの道具を紹介します。

  • constジェネリクス(const generics)を使うと、[i32; 3] の 3 のように、数値をジェネリックなパラメータにできます。
  • const fn を使うと、プログラムをビルドしている最中にコンパイラが関数を実行してくれます。プログラムが動き出す前に、答えがもう用意されているわけです。
  • PhantomData を使うと、余分なデータを何も持たずに、型に「これはユーザーのID」「この長さはフィート」といったラベルを付けられます。

卵のパックを思い浮かべてください。6個入りと12個入りは、どちらも「卵のパック」ですが、別の商品です。サイズはそのパックが何であるかの一部なのです。constジェネリクスを使うと、それをRustで表現できます。このページは急がずに読んでください。どの節もそれだけで読めるので、後半の節は必要になったときに戻ってきても大丈夫です。

constジェネリクス:数値をジェネリックなパラメータにする

Section titled “constジェネリクス:数値をジェネリックなパラメータにする”

Rustでは、配列の長さは型の一部です。[u32; 3] と [u32; 4] は、i32 と String くらい違う型です。そのため、[u32; 3] を受け取る関数に4個の要素は渡せません。

error[E0308]: mismatched types
--> src/main.rs:7:26
|
7 | println!("{}", total(basket));
| ----- ^^^^^^ expected an array with a size of 3, found one with a size of 4
| |
| arguments to this function are incorrect

「型が合わない。サイズ3の配列を期待したのに、サイズ4の配列が渡された」というエラーです。これを解決するのが constジェネリクス です。山かっこの中に const N: usize と書くと、その関数はどんな長さの配列でも受け取れるようになります。

fn summary<const N: usize>(prices: [u32; N]) -> String {
let mut sum = 0;
for price in prices {
sum += price;
}
// 関数の中では、N はただの普通の数値です
format!("{N} 個で合計 {sum} 円")
}
fn main() {
let small_basket = [4, 10, 2];
let big_basket = [3, 8, 1, 12, 6];
println!("{}", summary(small_basket));
println!("{}", summary(big_basket));
}

実行結果:

3 個で合計 16 円
5 個で合計 30 円
▶ 実際に試してみよう

<const N: usize> は「usize 型の任意の数 N について」と読みます。普通のジェネリクスで T を推論するのと同じように、Rustは渡された配列から N を推論します。そして普通のジェネリクスと同じく、コンパイラは使われた長さごとに summary の別々のコピーを作るので、実行時のコストはかかりません。

正直に言うと、この小さな関数なら、スライス(prices: &[u32])でも同じように動きますし、そのほうがシンプルです。スライスのページを見てください。constジェネリクスが本当に役立つのは、長さが型の一部でなければならないときです。たとえば、ヒープを使わずに構造体の中に保存したいときや、戻り値の型に使いたいときです。次はそれを見ていきましょう。

constジェネリクスを使った構造体:座席表

Section titled “constジェネリクスを使った構造体:座席表”

映画館の座席表を想像してください。いくつかの列があり、どの列にも同じ数の席があります。次の行列(matrix)は、行の数 R と列の数 C の両方が型の一部になっています。

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new(cells: [[i32; C]; R]) -> Self {
Matrix { cells }
}
fn size(&self) -> String {
format!("{R}x{C}")
}
// 行と列を入れ替える:R x C の行列が C x R の行列になる
fn transpose(&self) -> Matrix<C, R> {
let mut cells = [[0; R]; C];
for row in 0..R {
for col in 0..C {
cells[col][row] = self.cells[row][col];
}
}
Matrix { cells }
}
}
fn main() {
let seats = Matrix::new([[1, 2, 3], [4, 5, 6]]);
let flipped = seats.transpose();
println!("{} {:?}", seats.size(), seats.cells);
println!("{} {:?}", flipped.size(), flipped.cells);
}

実行結果:

2x3 [[1, 2, 3], [4, 5, 6]]
3x2 [[1, 4], [2, 5], [3, 6]]
▶ 実際に試してみよう

transpose の戻り値の型 Matrix<C, R> に注目してください。2x3 の行列を入れ替えると 3x2 になることを、コンパイラは知っています。もし中で間違えて、違う形の行列を作ってしまったら、コンパイルが通りません。表全体が構造体の中に収まっていて、Vec もヒープも使っていません。impl<const R: usize, const C: usize> の行は impl<T> と同じ働きで、「これらのメソッドはどのサイズにもある」という意味です。

実用的な例:固定サイズのリングバッファ

Section titled “実用的な例:固定サイズのリングバッファ”

車のドライブレコーダーは、直近の数分間の映像だけを残します。いっぱいになると、新しい録画がいちばん古い録画を上書きします。これがリングバッファ(ring buffer)で、constジェネリクスにぴったりの題材です。容量が決まっているので、要素を配列に保存できるからです。

このリングバッファは型 T とサイズ N の両方についてジェネリックなので、impl の行は impl<T, const N: usize> になります。

struct RingBuffer<T, const N: usize> {
items: [T; N],
next: usize, // 次に push する位置
len: usize, // 保存している要素の数(N を超えることはない)
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
fn new() -> Self {
RingBuffer { items: [T::default(); N], next: 0, len: 0 }
}
fn push(&mut self, item: T) {
self.items[self.next] = item; // いっぱいのときは、いちばん古い要素を上書きする
self.next = (self.next + 1) % N;
if self.len < N {
self.len += 1;
}
}
// 保存している要素を、古い順に返す
fn to_vec(&self) -> Vec<T> {
let start = (self.next + N - self.len) % N;
(0..self.len).map(|i| self.items[(start + i) % N]).collect()
}
}
fn main() {
// 直近3回分の気温だけを残す
let mut recent: RingBuffer<i32, 3> = RingBuffer::new();
for temp in [18, 19, 21, 24, 22] {
recent.push(temp);
println!("{temp} を追加: {:?}", recent.to_vec());
}
}

実行結果:

18 を追加: [18]
19 を追加: [18, 19]
21 を追加: [18, 19, 21]
24 を追加: [19, 21, 24]
22 を追加: [21, 24, 22]
▶ 実際に試してみよう

[T::default(); N] は、デフォルト値(i32 なら 0)を N 個並べた配列を作ります。このように値を繰り返すには T: Copy が必要です。だから impl で Copy + Default を求めているのです。RingBuffer<i32, 3> と RingBuffer<i32, 60> は別の型なので、60スロットのバッファが必要な場所に3スロットのバッファを渡してしまうことは絶対にありません。標準ライブラリの、サイズが伸び縮みするバージョンを使いたいなら、VecDeque と BinaryHeapのページを見てください。

デフォルト値、波かっこ、そして現在の制限

Section titled “デフォルト値、波かっこ、そして現在の制限”

constパラメータには const N: usize = 4 のようにデフォルト値を付けられます。また、名前の付いた定数や、波かっこで囲んだ式を渡すこともできます。

struct Buffer<const N: usize = 4> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self {
Buffer { data: [0; N] }
}
}
const PAGE: usize = 8;
fn main() {
let small: Buffer = Buffer::new(); // 数を書かない:デフォルトの 4
let page = Buffer::<PAGE>::new(); // 名前の付いた定数
let double = Buffer::<{ PAGE * 2 }>::new(); // 式には { } が必要
println!("{} {} {}", small.data.len(), page.data.len(), double.data.len());
}

実行結果:

4 8 16
▶ 実際に試してみよう

デフォルト値が使われるのは、let small: Buffer のように、数を書かずに型を書いたときだけです。let small = Buffer::new(); とだけ書くと、error[E0284]: type annotations needed for Buffer<_>(型注釈が必要)というエラーになります。Rustは式からデフォルト値を推測しないからです。

安定版(stable)のRustのconstジェネリクスは便利ですが、あえて機能が絞られています。ぶつかる前に、どこが限界なのかを知っておくと安心です。

  • 使える型: 整数(usize、u8、i32 など)、bool、char だけです。const NAME: &'static str と書くと、“&'static str is forbidden as the type of a const generic parameter”(&'static str はconstジェネリックパラメータの型として使えない)というエラーになります。
  • 型の中でジェネリックなパラメータを使った計算はできません。 ジェネリックな構造体やシグネチャの中で [f32; N * 2] と書くとエラーになります(“generic parameters may not be used in const operations”)。N をそのまま使うのは問題ありません。また、N がただの値として扱われる関数の本体の中なら、N * 2 も書けます。型の中で N * 2 を書けるようにする機能(generic_const_exprs)は、まだnightly版でしか使えません。
  • 値のチェック: 「N は0より大きくなければならない」のような境界は書けません。いちばん近いのは、関数の中にインラインの const ブロックを書く方法です。const { assert!(N > 0, "need at least one slot") }; と書くと、間違った N でプログラムをビルドしたときに失敗します(cargo check では見つからないこともあります)。

const fn:計算はコンパイラにまかせる

Section titled “const fn:計算はコンパイラにまかせる”

お店の中には、レジで毎回値段を計算するのではなく、価格表を一度だけ印刷しておくところがあります。const fn は、プログラムのビルド中にコンパイラが実行してもよい関数です。つまり、答えはあらかじめプログラムの中に「印刷」されています。

const の定数には、変数のページですでに出会いました。その値はコンパイル時にわかっていなければならないので、普通はリテラルと簡単な計算しか使えません。const fn を使えば、そこで自分の関数も呼べるようになります。

const fn square(n: usize) -> usize {
n * n
}
const BOARD_SIDE: usize = 4;
const CELLS: usize = square(BOARD_SIDE); // コンパイラが計算する
fn main() {
// 配列の長さはコンパイル時にわかっていなければならない。CELLS はわかっている
let board = [0u8; CELLS];
println!("{BOARD_SIDE}x{BOARD_SIDE} の盤面のマスは {} 個", board.len());
println!("実行時に計算した square(12) は {}", square(12));
}

実行結果:

4x4 の盤面のマスは 16 個
実行時に計算した square(12) は 144
▶ 実際に試してみよう

const fn も普通の関数です。main の中の square(12) は、実行時のごく普通の呼び出しです。コンパイル時に実行しなければならないのは、const コンテキストで使われたときだけです。const コンテキストとは、const や static の定義、配列の長さ、constジェネリクスの引数、const { ... } ブロックのことです。

よくある使い道がルックアップテーブル(lookup table)です。値の一覧をコンパイル時に一度だけ計算しておき、実行時にはそれを読むだけにします。次の例では、うるう年ではない年について「各月の初日の前までに何日あるか」の表をコンパイラが作ります。

const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const fn days_before_each_month() -> [u32; 12] {
let mut table = [0; 12];
let mut month = 1;
while month < 12 {
table[month] = table[month - 1] + DAYS_IN_MONTH[month - 1];
month += 1;
}
table
}
// コンパイラが一度だけ作る。実行時には、ただの12個の数値
const DAYS_BEFORE: [u32; 12] = days_before_each_month();
fn day_of_year(month: usize, day: u32) -> u32 {
DAYS_BEFORE[month - 1] + day
}
fn main() {
println!("{:?}", DAYS_BEFORE);
println!("3月1日は1年の {} 日目", day_of_year(3, 1));
println!("12月25日は1年の {} 日目", day_of_year(12, 25));
}

実行結果:

[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
3月1日は1年の 60 日目
12月25日は1年の 359 日目
▶ 実際に試してみよう

for ループではなく while ループを使っていることに気づいたでしょうか。これは好みの問題ではありません。現在、const fn の中で使えるものは次のとおりです。

  • let と let mut、算術演算、比較、if、match、loop、while
  • 配列、タプル、構造体、列挙型、そして table[month] のようなインデックスアクセス
  • ほかの const fn の呼び出し(u32::pow や slice.len() など、標準ライブラリにもconstな関数がたくさんあります)
  • &mut 参照と浮動小数点数の計算(それぞれRust 1.83と1.82から安定化)
  • panic! と assert!(コンパイル時に実行されると、コンパイルエラーになります)

そして、まだ使えないものはこちらです。for ループ(Iterator トレイトが必要ですが、安定版ではトレイトのメソッドをconstにできません)、String::from や println! のような普通の関数の呼び出し、そして Vec への push のようにヒープにメモリを確保するものすべてです。

const と static のどちらを使う? どちらも変わらないグローバルな値ですが、保存のされ方が違います。

const static
どこにあるか 使う場所それぞれにコピーされる メモリ上の決まった1か所
アドレス 使うたびに違うこともある 常に同じで、プログラムが終わるまで生きている('static)
可変にできるか できない static mut はあるが、unsafe が必要
何に使うか 数値、小さな表、設定値 1か所にだけ保存したい大きな表、参照が必要なグローバル変数

迷ったら const を使いましょう。変更できるグローバル変数が必要なら、static mut ではなく、中にアトミック型や Mutex を入れた static を使います。共有状態のページを見てください。

PhantomData:取り違えようのないラベル

Section titled “PhantomData:取り違えようのないラベル”

実際のシステムでいつも起きているバグがあります。ユーザーIDも注文IDもただの数値なので、cancel_order にユーザーIDを渡してしまっても、何も止めてくれません。型にラベルを付けて、Id<User> と Id<Order> を区別したいところです。最初はこう書いてみたくなるでしょう。

struct Id<T> {
value: u32,
}
error[E0392]: type parameter `T` is never used
--> src/main.rs:1:11
|
1 | struct Id<T> {
| ^ unused type parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`

Rustは、どのフィールドでも使われていない型パラメータを受け付けません(「型パラメータ T が使われていない」)。直し方は、コンパイラが提案しているとおりです。std::marker::PhantomData<T> は、「この構造体は T に関係している」と宣言するだけで、中身は何も持たないフィールドです。サイズは0バイトです。

use std::marker::PhantomData;
struct User;
struct Order;
struct Id<T> {
value: u32,
_marker: PhantomData<T>, // 0バイト。ただのラベル
}
impl<T> Id<T> {
fn new(value: u32) -> Self {
Id { value, _marker: PhantomData }
}
}
fn find_user(id: &Id<User>) -> &'static str {
if id.value == 7 { "アマラ" } else { "不明" }
}
fn cancel_order(id: &Id<Order>) {
println!("注文 #{} をキャンセルします", id.value);
}
fn main() {
let user_id: Id<User> = Id::new(7);
let order_id: Id<Order> = Id::new(7);
println!("ユーザー #{} は{}さん", user_id.value, find_user(&user_id));
cancel_order(&order_id);
println!("Id のサイズは {} バイト", std::mem::size_of::<Id<User>>());
}

実行結果:

ユーザー #7 はアマラさん
注文 #7 をキャンセルします
Id のサイズは 4 バイト
▶ 実際に試してみよう

どちらのIDも数値の7を持っていて、サイズはどちらも普通の u32 とまったく同じ4バイトです。では、cancel_order(&user_id); と書いてみましょう。

error[E0308]: mismatched types
--> src/main.rs:32:18
|
32 | cancel_order(&user_id);
| ------------ ^^^^^^^^ expected `&Id<Order>`, found `&Id<User>`
| |
| arguments to this function are incorrect

&Id<Order> を期待したのに &Id<User> が渡された、というエラーです。取り違えは、プログラムを実行する前に見つかります。PhantomData は型であると同時に値でもあります。だから構造体を作るときに _marker: PhantomData と書くのです。先頭の _ は、「このフィールドは読まない」という意味でよく使われる名前の付け方にすぎません。

単位:メートルとフィートは違う

Section titled “単位:メートルとフィートは違う”

1999年、NASAは火星探査機マーズ・クライメイト・オービターを失いました。あるソフトウェアがヤード・ポンド法の数値を出力し、別のソフトウェアはメートル法の数値を期待していたからです。どちらも「ただの数値」でした。PhantomData を使えば単位が型の一部になり、1つのジェネリックな Length<U> ですべての単位を扱えます。

use std::marker::PhantomData;
use std::ops::Add;
struct Meters;
struct Feet;
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
}
// すべての単位に使える1つの impl。ただし両辺は同じ単位でなければならない
impl<U> Add for Length<U> {
type Output = Length<U>;
fn add(self, other: Length<U>) -> Length<U> {
Length::new(self.value + other.value)
}
}
impl Length<Feet> {
fn to_meters(self) -> Length<Meters> {
Length::new(self.value * 0.3048)
}
}
fn main() {
let run: Length<Meters> = Length::new(400.0) + Length::new(250.0);
let climb: Length<Feet> = Length::new(1000.0);
let climb_m = climb.to_meters();
println!("登った高さ: {:.1} m", climb_m.value);
let total = run + climb_m;
println!("合計: {:.1} m", total.value);
}

実行結果:

登った高さ: 304.8 m
合計: 954.8 m
▶ 実際に試してみよう

to_meters() を忘れて run + climb と書くと、Rustは受け付けません。

error[E0308]: mismatched types
--> src/main.rs:39:23
|
39 | let total = run + climb;
| ^^^^^ expected `Length<Meters>`, found `Length<Feet>`

高度なトレイトのページでは、Meters と Centimeters は別々の構造体で、それぞれに Add の実装がありました。ここでは、impl<U> Add for Length<U> が1つあるだけで、これから足すことになるどんな単位にも使えます。

型状態(typestate):開いたまま鍵をかけられないドア

Section titled “型状態(typestate):開いたまま鍵をかけられないドア”

同じ技を使って、ものの状態を追いかけることもできます。ドアは「開いている」「閉まっている」「鍵がかかっている」のどれかです。閉まったドアには鍵をかけられますが、開いたドアに鍵をかけるのは意味がありません。状態のラベルを型に持たせると、ありえない操作はそもそも存在しなくなります。

use std::marker::PhantomData;
struct Open;
struct Closed;
struct Locked;
struct Door<State> {
room: String,
_state: PhantomData<State>,
}
// どの状態でも使える
impl<State> Door<State> {
fn room(&self) -> &str {
&self.room
}
// 同じドアに、新しい状態のラベルを付ける
fn into_state<Next>(self, action: &str) -> Door<Next> {
println!("{}: {action}", self.room);
Door { room: self.room, _state: PhantomData }
}
}
impl Door<Closed> {
fn new(room: &str) -> Self {
Door { room: room.to_string(), _state: PhantomData }
}
fn open(self) -> Door<Open> {
self.into_state("開けました")
}
fn lock(self) -> Door<Locked> {
self.into_state("鍵をかけました")
}
}
impl Door<Open> {
fn close(self) -> Door<Closed> {
self.into_state("閉めました")
}
}
impl Door<Locked> {
fn unlock(self) -> Door<Closed> {
self.into_state("鍵を開けました")
}
}
fn main() {
let door = Door::new("サーバールーム").open().close().lock();
println!("{}は夜間ロック中です", door.room());
let door = door.unlock().open();
println!("{}がまた開きました", door.room());
}

実行結果:

サーバールーム: 開けました
サーバールーム: 閉めました
サーバールーム: 鍵をかけました
サーバールームは夜間ロック中です
サーバールーム: 鍵を開けました
サーバールーム: 開けました
サーバールームがまた開きました
▶ 実際に試してみよう

では、開いたドアに鍵をかけてみましょう。Door::new("サーバールーム").open().lock() です。

error[E0599]: no method named `lock` found for struct `Door<Open>` in the current scope
--> src/main.rs:50:44
|
50 | let door = Door::new("サーバールーム").open().lock();
| ^^^^ method not found in `Door<Open>`
|
= note: the method was found for `Door<Closed>`

「Door<Open> には lock というメソッドがない。Door<Closed> にならある」というエラーです。どのメソッドも self を値で受け取るので、古いドアはそこで使い切られます。開けた後に「閉まっている」ドアを使い続けることはできません。デザインパターンのページでは、それぞれの状態が自分のデータも持つ、同じ型状態の考え方を紹介しています。状態がただのラベルでよいときは、PhantomData を使った軽いやり方が向いています。

特定のサイズだけのメソッドと関連定数

Section titled “特定のサイズだけのメソッドと関連定数”

ジェネリクスのページで impl Pair<f64> を見ました。具体的な1つの型だけにメソッドを用意する書き方です。constジェネリクスでも同じことができます。特定のサイズでしか意味のないメソッドもあるからです。

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
// 正方行列だけ:行と列が同じ N
impl<const N: usize> Matrix<N, N> {
fn trace(&self) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += self.cells[i][i];
}
sum
}
}
// 2x2 の行列だけ
impl Matrix<2, 2> {
fn determinant(&self) -> i32 {
let [[a, b], [c, d]] = self.cells;
a * d - b * c
}
}
fn main() {
let small = Matrix { cells: [[3, 8], [4, 6]] };
let big = Matrix { cells: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] };
println!("small: トレース {}、行列式 {}", small.trace(), small.determinant());
println!("big: トレース {}", big.trace());
}

実行結果:

small: トレース 9、行列式 -14
big: トレース 15
▶ 実際に試してみよう

big.determinant() はコンパイルできませんし、2x3 の行列に trace() を呼ぶこともできません。error[E0599]: no method named trace found for struct Matrix<2, 3> のようなエラーになり、the method was found for Matrix<N, N>(そのメソッドは Matrix<N, N> にある)という注記が付きます。「トレースを持つのは正方行列だけ」というルールが、型で表現されたわけです。

関連定数(associated const)は、メソッドと同じように、型やトレイトに属する定数です。トレイトの中に書くと、実装ごとに自分の値を決められます。ジェネリックなコードからは U::NAME の形で読めます。さっきの単位に、記号とサイズを持たせてみましょう。

use std::fmt;
use std::marker::PhantomData;
trait Unit {
const SYMBOL: &'static str;
const IN_METERS: f64; // この単位1つが何メートルか
}
struct Meters;
struct Feet;
struct Miles;
impl Unit for Meters {
const SYMBOL: &'static str = "m";
const IN_METERS: f64 = 1.0;
}
impl Unit for Feet {
const SYMBOL: &'static str = "ft";
const IN_METERS: f64 = 0.3048;
}
impl Unit for Miles {
const SYMBOL: &'static str = "mi";
const IN_METERS: f64 = 1609.344;
}
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U: Unit> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
// 定数を使うので、どの2つの単位の間でも変換できる
fn convert<To: Unit>(&self) -> Length<To> {
Length::new(self.value * U::IN_METERS / To::IN_METERS)
}
}
impl<U: Unit> fmt::Display for Length<U> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2} {}", self.value, U::SYMBOL)
}
}
fn main() {
let marathon: Length<Miles> = Length::new(26.2);
let everest: Length<Feet> = Length::new(29032.0);
println!("{} = {}", marathon, marathon.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Miles>());
}

実行結果:

26.20 mi = 42164.81 m
29032.00 ft = 8848.95 m
29032.00 ft = 5.50 mi
▶ 実際に試してみよう

U::IN_METERS はコンパイル時に決まるので、実行時に表を探す必要はありません。新しい単位、たとえば Kilometers を追加するのも、小さな impl Unit ブロックを1つ書くだけです。convert と Display はすぐにその単位でも使えます。固有の impl にも関連定数を持たせられます。たとえば impl<T, const N: usize> RingBuffer<T, N> { const CAPACITY: usize = N; } と書けば、RingBuffer::<i32, 3>::CAPACITY として読めます。

条件を満たすすべての型に1つのトレイト実装を用意したいなら、それはブランケット実装です。高度なトレイトのページで紹介しています。

これらの道具を使わないほうがよいとき

Section titled “これらの道具を使わないほうがよいとき”

ここで紹介した機能は強力なので、つい使いたくなります。正直なアドバイスをいくつか書いておきます。

  • スライスや Vec で十分なことが多いです。 関数が要素を読むだけなら、&[T] はどんな長さでも受け取れて、読みやすくもあります。長さが実行時にしかわからない場合(ファイルやユーザーの入力など)は、どのみち Vec が必要です。
  • 大きな配列はスタックに置かれます。 RingBuffer<u8, 10_000_000> は構造体の中に10メガバイトを抱えることになり、スタックがあふれる(スタックオーバーフロー)おそれがあります。Box に入れる(スマートポインタのページを参照)か、Vec を使いましょう。
  • 新しいサイズや型を使うたびに、コードのコピーが1つ増えます。 ジェネリックなコードを20通りの N で使えば、20個のバージョンがビルドされます。そのぶんコンパイル時間とバイナリサイズが増えます。パフォーマンスのページを見てください。
  • 型状態を使うと、型が増えてエラーメッセージも長くなります。 多くの人が使うAPI(ビルダー、接続、決済の流れなど)にはとても向いています。でも、データベースから読み込んだステータスのように、実行時のデータで変わる状態には、列挙型と match を使いましょう。

おすすめのルールはこうです。まずはシンプルに書き始めて、これらの道具で防げる本物のバグや本物のコストが見えてきたら、そこで使いましょう。

型の中でconstパラメータを使って計算する。 自然に書けそうに思えますが、安定版のRustではまだできません。

struct Pairs<const N: usize> {
values: [f32; N * 2],
}
error: generic parameters may not be used in const operations
--> src/main.rs:2:19
|
2 | values: [f32; N * 2],
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`

「ジェネリックなパラメータはconstの演算には使えない」というエラーです。直し方は、[[f32; 2]; N](ペアの配列)を使う、パラメータをもう1つ追加する、あるいはサイズを型に入れる必要がないなら Vec を使う、のどれかです。

式を波かっこで囲み忘れる。 Buffer::<2 + 2>::new() と書くと、error: expressions must be enclosed in braces to be used as const generic arguments(式をconstジェネリクスの引数にするには波かっこで囲む必要がある)というエラーになります。help の行に直し方 Buffer::<{ 2 + 2 }>::new() が出ます。ただの数値や const の名前なら、波かっこはいりません。

const コンテキストで const ではないものを呼ぶ。 const の中では普通の関数を呼べませんし、const fn の中では for ループを使えません。

const GREETING: String = String::from("hello");
error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants
--> src/main.rs:1:26
|
1 | const GREETING: String = String::from("hello");
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants

「定数の中ではconstではない関数を呼べない」というエラーです。文字列なら const GREETING: &str = "hello"; と書きましょう。const fn の中では、上のルックアップテーブルのように、for の代わりに while ループとインデックスを使います。

PhantomData を持つ構造体に #[derive(Clone, Copy)] を付ける。 Id<User> は u32 を1つ持っているだけなので、Copy になると思いますよね。ところが derive は T: Copy という境界を追加します。User は Copy ではないので、IDはコピーされずにムーブされてしまいます。

error[E0382]: use of moved value: `id`
--> src/main.rs:16:10
|
14 | let id: Id<User> = Id { value: 1, _marker: PhantomData };
| -- move occurs because `id` has type `Id<User>`, which does not implement the `Copy` trait
15 | show(id);
| -- value moved here
16 | show(id);
| ^^ value used here after move
|
note: if all bounds were met, you could clone the value
--> src/main.rs:4:1
|
3 | #[derive(Clone, Copy)]
| ----- derived `Clone` adds implicit bounds on type parameters

いちばん簡単な直し方は、目印の型にも Clone, Copy を derive することです(#[derive(Clone, Copy)] struct User;)。あるいは、T に境界を必要としない実装を手で書きます。

impl<T> Clone for Id<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Id<T> {}

点数を、ちょうど N 個の要素を持つ配列にコピーする fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] を書いてください。足りない場所は 0 で埋め、余った点数は捨てます。pad_scores::<5>(&[90, 85]) と let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]); の2通りで呼んでみましょう。ヒント: [0; N] から始めて、0..N をループしながら scores.get(i) を使います。

解答を見る
fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] {
let mut out = [0; N];
for i in 0..N {
if let Some(&score) = scores.get(i) {
out[i] = score;
}
}
out
}
fn main() {
let five = pad_scores::<5>(&[90, 85]);
let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);
println!("{:?}", five);
println!("{:?}", top3);
}

実行結果:

[90, 85, 0, 0, 0]
[70, 95, 88]
▶ 実際に試してみよう

ここでは N を引数から推論できません(スライスの型には長さが含まれないからです)。そのため N は、ターボフィッシュ ::<5> か、求める型 [u32; 3] から決まります。

2. コンパイル時に決まるタイムアウト

Section titled “2. コンパイル時に決まるタイムアウト”

秒数を返す2つの const fn、minutes(n: u64) -> u64 と hours(n: u64) -> u64 を書いてください。それを使って const SESSION_TIMEOUT: u64 = hours(1) + minutes(30); と、1分・5分・15分の配列 const RETRY_DELAYS: [u64; 3] を定義し、両方を表示します。ヒント: hours から minutes を呼べます。

解答を見る
const fn minutes(n: u64) -> u64 {
n * 60
}
const fn hours(n: u64) -> u64 {
minutes(n * 60)
}
const SESSION_TIMEOUT: u64 = hours(1) + minutes(30);
const RETRY_DELAYS: [u64; 3] = [minutes(1), minutes(5), minutes(15)];
fn main() {
println!("セッションのタイムアウト: {SESSION_TIMEOUT} 秒");
println!("再試行の待ち時間: {:?}", RETRY_DELAYS);
}

実行結果:

セッションのタイムアウト: 5400 秒
再試行の待ち時間: [60, 300, 900]
▶ 実際に試してみよう

5400 という値も待ち時間の表も、コンパイラがビルド中に計算します。それに、ただの 5400 よりも hours(1) + minutes(30) のほうがずっと読みやすいですね。minutes から const を外すと、const の定義が E0015 でコンパイルできなくなります。

struct Euro; と struct Dollar;、そして cents: i64 フィールドと PhantomData<C> フィールドを持つ Money<C> を作ってください。new(cents) と plus(self, other: Money<C>) -> Money<C> を用意し、ユーロでランチ代とコーヒー代を足して合計を表示します。ヒント: other は同じ C を持つので、Money<Euro> と Money<Dollar> は足せません。

解答を見る
use std::marker::PhantomData;
struct Euro;
struct Dollar;
struct Money<C> {
cents: i64,
_currency: PhantomData<C>,
}
impl<C> Money<C> {
fn new(cents: i64) -> Self {
Money { cents, _currency: PhantomData }
}
fn plus(self, other: Money<C>) -> Money<C> {
Money::new(self.cents + other.cents)
}
}
fn main() {
let lunch: Money<Euro> = Money::new(1250);
let coffee: Money<Euro> = Money::new(320);
let ticket: Money<Dollar> = Money::new(4999);
let total = lunch.plus(coffee);
println!("合計: {}.{:02} EUR", total.cents / 100, total.cents % 100);
println!("チケット: {}.{:02} USD", ticket.cents / 100, ticket.cents % 100);
}

実行結果:

合計: 15.70 EUR
チケット: 49.99 USD
▶ 実際に試してみよう

lunch.plus(ticket) と書いてみてください。expected Money<Euro>, found Money<Dollar> というエラーで失敗します。また、金額を f64 ではなく整数のセントで持っておくと、お金の計算で丸め誤差が出るのも防げます。

  • constジェネリクス(const N: usize)を使うと、数値を型の一部にできます。長さの違う配列は別の型で、impl<T, const N: usize> と書けば、すべてのサイズに使えるメソッドを書けます。
  • 安定版のRustでは、constパラメータに使えるのは整数、bool、char です。式を引数にするには波かっこ({ PAGE * 2 })が必要で、型の中でジェネリックな N を使った計算はまだできません。
  • const fn は、const コンテキスト(const、static、配列の長さ)ではコンパイル時に実行されます。for の代わりに while を使い、呼べるのはほかの const fn だけです。
  • PhantomData<T> は、構造体を、実際には保存していない型に結び付けるサイズ0のフィールドです。型付きID、単位、型状態に使えます。
  • impl Matrix<2, 2> や impl<const N: usize> Matrix<N, N> で、特定のサイズだけにメソッドを用意できます。U::IN_METERS のような関連定数を使うと、型ごとにコンパイル時の値を持たせられます。
  • これらの道具は、コンパイル時間と読みやすさを犠牲にします。本物のバグを防げるときに使いましょう。

理解度チェック

1. [u32; 3] を受け取る関数に [u32; 4] を渡せないのはなぜですか?

2. PhantomData<T> は、実行時に構造体に何を追加しますか?

3. 安定版のRustで、const fn の中で使えないのはどれですか?

4. impl<const N: usize> Matrix<N, N> はどういう意味ですか?

スコア: 0 / 4