Rust の関数のローカルスコープで static mut な変数を扱う
たまに、ローカルスコープ内で可変な静的変数を宣言して使いたいなーと思うことがあります。C++ で書くとこういうことです。
#include <iostream>
void count_local_static() {
static uint32_t n {5};
std::cout << n++ << std::endl;
}
int main() {
count_local_static();
count_local_static();
count_local_static();
}
// 5
// 6
// 7
Rust にそのまま移植すると起きる問題と解決策
これを普通に Rust で書こうとすると、static mut にアクセスするのは unsafe だ、というエラーが起きます。
fn count_local_static() {
static mut COUNT: u32 = 5;
println!("{}", COUNT);
COUNT += 1;
}
fn main() {
count_local_static();
count_local_static();
count_local_static();
}
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
--> src\main.rs:3:20
|
3 | println!("{}", COUNT);
| ^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
--> src\main.rs:4:5
|
4 | COUNT += 1;
| ^^^^^^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
これを避けるには、ひとまず下記の4つのやり方がありそうです。それぞれまとめてみます。
-
unsafeを許容する -
std::sync::atomicな型を使う (atomicな型のみ) - 内部可変性を使う (
thread_local!とRefCell<T>) - 内部可変性を使う (
once_cellとMutex<RefCell<T>>)
unsafe を使うことを許容する
unsafe を使うことをいとわなければ、下記のようにシンプルに書くことが出来ます。関数ローカルでしか使わない前提で、unsafe を使うことに抵抗がなければ、これで十分かもしれません。
fn count_local_static() {
static mut COUNT: u32 = 5;
println!("{}", unsafe { COUNT });
unsafe {
COUNT += 1;
}
}
fn main() {
count_local_static();
count_local_static();
count_local_static();
}
std::sync::atomic な型を使用する
もし目的とする型が atomic な型であれば、unsafe を使わずに書きのように書くことができます。
use std::sync::atomic::{AtomicU32, Ordering};
fn count_local_static() {
static COUNT: AtomicU32 = AtomicU32::new(5);
println!("{}", COUNT.fetch_add(1, Ordering::SeqCst));
}
fn main() {
count_local_static();
count_local_static();
count_local_static();
}
atomic に関する詳細はこちらを参照。
内部可変性を使う (thread_local! と RefCell<T>)
atomic でない型を unsafe を使わずに関数ローカルで使いたい場合、thread_local! でスレッドローカルな変数を関数内で定義して使うのが一つのやり方です。ただし、ミュータブルに使用するためには RefCell を使用して、内部可変性を利用する必要があります。
use std::cell::RefCell;
fn count_local_static() {
thread_local!(
pub static COUNT: RefCell<u32> = RefCell::new(5u32)
);
COUNT.with(|count| {
let mut c = count.borrow_mut();
println!("{}", *c);
*c += 1;
})
}
fn main() {
count_local_static();
count_local_static();
count_local_static();
}
内部可変性や thread_local! については、こちらを参照ください。
thread_local! を使った内部可変性の実現についてや、static な変数についてはこちらの素晴らしい記事が詳しいです。
内部可変性を使う (once_cell と Mutex<RefCell<T>>)
上記の thread_local! な例は、once_cell と Mutex<RefCell<T>> などを使っても代替できます。Sync が要求されるため、関数ローカルなのに Mutex が必要になってしまいますが、見た目は多少マシになるかもしれません。
use once_cell::sync::Lazy;
use std::cell::RefCell;
use std::sync::Mutex;
fn count_local_static() {
static COUNT: Lazy<Mutex<RefCell<u32>>> = Lazy::new(|| Mutex::new(RefCell::new(5u32)));
let c_mtx = COUNT.lock().unwrap();
let mut c = c_mtx.borrow_mut();
println!("{}", *c);
*c += 1;
}
fn main() {
count_local_static();
count_local_static();
count_local_static();
}
まとめ
Rust で関数ローカルスコープな static mut 変数を扱う方法をいろいろ試してみました。今回は下記の4通りで実現できることがわかりました。
-
unsafeを許容する -
std::sync::atomicな型を使う (atomicな型のみ) - 内部可変性を使う (
thread_local!とRefCell<T>) - 内部可変性を使う (
once_cellとMutex<RefCell<T>>)
よりスマートな方法をご存知のかたは、ぜひ教えていただけるとありがたいです。
Discussion
最近Rustの勉強を始めたばかりでとても参考になりました。
4つ目の例は
RefCellを抜きにしてonce_cellとMutex<T>で事足りるのではないかと思いました。