🧣

[Rust] マーカトレイトから見る言語仕様

4 min read

はじめに

本記事はRust Advent Calendar 2021の22日目の記事です。

この記事では std::marker に含まれるマーカトレイトを実装しているかいないかで

  • ArcとかCell等のスマートポインタ使い分け
  • CloneとCopyの使い分け

などの違いを説明することを目指します。説明を簡略化するために、ある型TがトレイトAを実装する場合、TAであるといいます。

マーカートレイト

マーカトレイトはコンパイラの動作を決定するのに使われ、コンパイラから特別扱いされているトレイトです。空のトレイトなので、Copyであれば次のように実装することができます

impl Copy for MyStruct {}

これらのマーカートレイトはstd::markerに含まれています。ここではSync,Send,Sized,Copy を扱います。

https://doc.rust-lang.org/core/marker/index.html

Sizedトレイト

Rustにおける変数束縛に関わるマーカートレイトとしてstd::marker::Sizedがあります。

https://doc.rust-lang.org/std/marker/trait.Sized.html
pub trait Sized {}

もし、Sizedを実装されていない場合

  • 変数に束縛できず、関数の引数や戻り値にできない。
  • 型への参照はfat pointerになる。

参照やほとんどの型には暗黙のうちに実装されています。実装していない型をDST(Dynamic Sized Type)といい、例えば次があります。

  • トレイトオブジェクト
  • str
  • スライス

これらの型はSizedでないため、プログラム内ではfat pointerを通して扱います。fat pointerのメモリレイアウトは

  1. ポインタ(8byte)
  2. 追加情報(8byte)

追加情報はスライスなら要素数、strならバイト数など、サイズを決定するために必要なデータが含まれます。Sizedは手動で実装ができず、DST以外の型には自動で実装されます。

💡 トレイト境界には ?Sizedと書くことができます。これはSizedを実装していなくてもよいという意味です。

Copyトレイト

Rustにおけるムーブセマンティクスを制御するマーカトレイトとしてstd::marker::Copyがあります。

https://doc.rust-lang.org/std/marker/trait.Copy.html

Copyである型はムーブセマンティクスの代わりにコピーセマンティクスが発生します。ある型TCopyであるには以下の条件を満たしている必要があります。

  • Cloneである。
  • 単純なバイトコピーが可能である。
  • 型Tが構造体や列挙型なら、そのフィールドが全てCopyを実装している。

Cloneがただのバイトコピーとして実装される場合に限り、Copyです。例えば、ArcCloneするときに参照カウンタをインクリメントする必要があるので単純なバイトコピーができません。

ここを参考にすると、ムーブについては次のような3つに分類できます。

  • 値がムーブするlinear
  • 値がムーブするがCloneすることができるCloneable
  • 値がムーブするときにコピーされるCopyable
特性 semantics Clone Copy バイトコピー
Linear Move x x 不可
Cloneable Move x 不可
Copyable Copy

Linearというの線形型システムから来ていると思われます。

不変参照はCopyであるが可変参照はCopyではありません。&mut T(可変参照)は1つしか存在できないのでCopyすることができません。一方で不変参照は複数存在できるのでCopyできます。

Send・Syncトレイト

std::marker::Sendstd::marker::Syncは並列処理の安全性を保証するのに使われるマーカトレイトです。

https://doc.rust-lang.org/std/marker/trait.Send.html
https://doc.rust-lang.org/std/marker/trait.Sync.html

この二つである型は次のような性質を持ちます。

  • Send 所有権を別スレッドに移動できる型。
  • Sync 参照(&T)を別スレッドに移動できる型。型Tの参照がSendである場合にのみ、Syncです。

ほとんどの型はデフォルトでSendとなっています。生ポインタはSendでもSyncでもありません。もし生ポインタのようなSendやSyncでもない型に実装したい場合、unsafe implでコンパイラに対してスレッドセーフであることを明示することになります。

unsafe impl Send for MyStruct {} 

SendSyncでない型を複数スレッドで共有したい場合、次のスマートポインタが利用できます。

  • Sendにしたい場合 Arcに包む
  • Syncにしたい場合 MutexRwLockに包む。

例えばArc<Mutex<T>> での利用例を見てみましょう。MutexはSend、Syncを実装しているので、参照を他のスレッドに移すことができます。しかし、MutexCloneを実装していないので、他のスレッドに移した後に元のスレッドから参照することができず、複数のスレッドからアクセスすることができなくなります。例えば次のコードでは、xがmoveされてしまうのでエラーになります。

use std::sync::Mutex;
use std::thread
fn main() {
	let x = Mutex::new(2);
	let child = thread::spawn(move|| {
		dbg!(x);
	});
	child.join().unwrap();
	dbg!(x); // error[E0382]: use of moved value: `x`
}

この場合、Sendを実装していてかつCloneができるArcで包む方法が有効です。Arc<Mutex<T>>は所有権を複数スレッドで共有しながら値を変更することが可能になります。

https://doc.rust-lang.org/std/sync/struct.Arc.html

RcRefCellは逆にSyncSendを実装しておらず、複数スレッドからの書き換えや所有権の移動ができません。

  • RcArcと違い参照カウンタがアトミックではないので、カウント中に割り込まれる可能性がある。そのため、他のスレッドに所有権を移すと、データ競合が生じる可能性があり、SendではなくSyncでもない。
  • RefCellの借用チェックはスレッドセーフではない。他のスレッドに所有権を移すと、元のスレッドからのアクセス権が失われてデータ競合が生じないので、SendだがSyncではない。

ここまで登場したRustのスマートポインタの実装しているトレイトまとめると次のようになる。

Send Sync Clone Copy
Rc x x o x
Arc o x o x
Cell o x o o
RefCell o x o x
Mutex o o x x

まとめ

Rustにおいては所有権が移動したことやスレッドセーフでないことをコンパイラが検知することができます。自動実装されるマーカートレイトを参考にコンパイラがスレッドセーフを保証してくれるRustの型システムの恩恵を感じます。

記事内容に誤りがあった場合、コメントで教えていただけると嬉しいです。

参考

https://rust-lang.github.io/rfcs/0019-opt-in-builtin-traits.html
https://ytakano.hatenablog.com/entry/2020/12/23/204528
https://doc.rust-lang.org/nomicon/send-and-sync.html
https://qiita.com/qnighy/items/4e7b9b93e7146306d20a

Discussion

ログインするとコメントできます