📚 背景知識(読んでから問題へ)
Rustにおける「スマートポインタ」とは、単なる生ポインタ以上の機能(所有権管理・参照カウント・借用チェックなど)を持つ、ポインタのように振る舞う構造体のことです。この「ポインタのように振る舞う」を型システムのレベルで実現しているのがstd::ops::Derefトレイトです。
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
*my_valueという単項演算子*(デリファレンス)を自分の型に対して使えるようにするのがDerefトレイトです。コンパイラは*my_valueという式を見つけると、内部的には*(my_value.deref())に展開します。標準ライブラリのBox<T>はまさにこのDeref<Target = T>を実装しており、Box<i32>を*boxed_i32のようにデリファレンスして中のi32を取り出せるのはこの仕組みによるものです。
さらに重要なのがDeref coercion(デリファレンス型強制)です。これは関数・メソッド呼び出しの引数位置で、&Tが&Uを要求する場所に渡された場合、T: Deref<Target = U>であればコンパイラが自動的に.deref()を挟んでくれる機能です。
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = Box::new(String::from("Rust"));
hello(&m); // &Box<String> → &String → &str と2段階のcoercionが自動適用される
}
&Box<String>を渡しているのに&strを受け取る関数がそのまま呼べるのは、Box<String>: Deref<Target = String>かつString: Deref<Target = str>という連鎖的なcoercionをコンパイラが自動適用しているためです。これにより、呼び出し側は毎回&*mやm.as_str()のような明示的な変換を書く必要がなくなります。
可変版がDerefMutです。
pub trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
注目すべきはDerefMut: Derefというスーパートレイト境界です。「書き込みできる(deref_mut)なら読み込みもできる(deref)はず」という自然な包含関係を、コンパイラがトレイト境界のレベルで強制しています。DerefMutだけを単独で実装することはできません。
📝 問題
以下の4つの要求に順番に答えてください。
要求1(概念理解)
次のコードがコンパイルが通り、かつ実行できる理由を、Derefトレイトの仕組み(*演算子の展開ルールとDeref coercion)に基づいて説明してください。
fn print_upper(s: &str) {
println!("{}", s.to_uppercase());
}
fn main() {
let boxed = Box::new(String::from("hello"));
print_upper(&boxed); // なぜ &Box<String> を &str が要求される場所に渡せるのか?
println!("first char via deref: {}", (*boxed).chars().next().unwrap());
}
要求2(実装: 独自スマートポインタとDeref coercion)
Box<T>と同じような「中身を1つだけ保持するだけのシンプルなラッパー型」MyBox<T>を自作し、Derefを実装してください。そのうえで、fn greet(name: &str)のような関数に&MyBox<String>を渡し、Deref coercionが2段階(MyBox<String> → String → str)で効くことをmain関数の中で実証してください。
要求3(実装: DerefMut)
読み書きのたびにログを出すLoggingBox<T>型を実装してください。
Deref実装:.deref()が呼ばれたら"[read] value accessed"を標準出力に出すDerefMut実装:.deref_mut()が呼ばれたら"[write] value accessed (mutably)"を標準出力に出すLoggingBox<i32>を作り、*logging_box += 10;のように中身を書き換えるコードを書き、書き込み時にDerefMut側のログが出ることを#[cfg(test)]のテストで検証してください(出力の検証ではなく、書き換え後の値が正しいことをテストで検証すれば十分です)
要求4(概念理解・設計判断)
DerefMutがDerefのスーパートレイトになっている(DerefMut: Deref)のはなぜだと思いますか。もし逆に「Derefを実装せずにDerefMutだけを実装できる」設計だったとしたら、どのような不自然な状況が起こり得るか、具体例を挙げて説明してください。
🔍 ヒント(段階的開示)
ヒント1 — 方向性
*exprという式は、exprが参照でも生ポインタでもない任意の型に対して書かれた場合、コンパイラがDerefトレイトのderefメソッド呼び出しに変換できないか探します- 関数・メソッド呼び出しの「引数を渡す位置」は、
*を明示的に書かなくても暗黙にcoercionが効く特別な場所です。逆にlet束縛や戻り値の型注釈のない代入では、coercionは自動的には効きません MyBox<T>はBox<T>の簡易版だと考えてください。中身をヒープに置く必要はなく、タプル構造体struct MyBox<T>(T);で十分です
ヒント2 — アプローチ
Deref::derefのシグネチャはfn deref(&self) -> &Self::Targetです。MyBox<T>ならTarget = Tとし、&self.0を返せば実装できますDerefMut::deref_mutのシグネチャはfn deref_mut(&mut self) -> &mut Self::Targetです。LoggingBox<T>はDerefとDerefMutの両方を実装する必要があります(DerefMutだけではDerefの要求を満たせずコンパイルエラーになります)*logging_box += 10;は「logging_boxを可変参照でデリファレンスして、その場所に+=する」という意味です。これは*DerefMut::deref_mut(&mut logging_box) += 10に展開されるため、必ずderef_mutが呼ばれます
ヒント3 — コード骨格
use std::ops::{Deref, DerefMut};
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct LoggingBox<T>(T);
impl<T> Deref for LoggingBox<T> {
type Target = T;
fn deref(&self) -> &T {
println!("[read] value accessed");
&self.0
}
}
impl<T> DerefMut for LoggingBox<T> {
fn deref_mut(&mut self) -> &mut T {
println!("[write] value accessed (mutably)");
&mut self.0
}
}
✅ 模範解答
use std::ops::{Deref, DerefMut};
// 要求2: Box<T> の簡易版。中身を1つ保持するだけのタプル構造体
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(value: T) -> MyBox<T> {
MyBox(value)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn greet(name: &str) {
println!("Hello, {name}!");
}
// 要求3: 読み書きでログを出す LoggingBox<T>
struct LoggingBox<T>(T);
impl<T> LoggingBox<T> {
fn new(value: T) -> LoggingBox<T> {
LoggingBox(value)
}
}
impl<T> Deref for LoggingBox<T> {
type Target = T;
fn deref(&self) -> &T {
println!("[read] value accessed");
&self.0
}
}
impl<T> DerefMut for LoggingBox<T> {
fn deref_mut(&mut self) -> &mut T {
println!("[write] value accessed (mutably)");
&mut self.0
}
}
fn main() {
// 要求1の再確認: Box<T> の Deref coercion
let boxed = Box::new(String::from("hello"));
print_upper(&boxed);
println!("first char via deref: {}", (*boxed).chars().next().unwrap());
// 要求2: MyBox<String> → String → str と2段階の coercion が効く
let m = MyBox::new(String::from("Rust"));
greet(&m); // &MyBox<String> をそのまま &str が要求される場所に渡せる
// 要求3: DerefMut による書き込み
let mut counter = LoggingBox::new(0);
println!("before: {}", *counter); // [read] ログが出る
*counter += 10; // [write] ログが出る(deref_mut 経由)
println!("after: {}", *counter); // [read] ログが出る
}
fn print_upper(s: &str) {
println!("{}", s.to_uppercase());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mybox_deref_coercion_reaches_str_methods() {
let m = MyBox::new(String::from("rust"));
// &MyBox<String> → &String → &str の coercion が効くことの確認
assert_eq!(m.len(), 4); // String::len (実際は str::len) が自動で呼べる
assert_eq!(&*m, "rust");
}
#[test]
fn loggingbox_deref_mut_actually_mutates_inner_value() {
let mut lb = LoggingBox::new(5);
assert_eq!(*lb, 5);
*lb += 10;
assert_eq!(*lb, 15);
}
#[test]
fn loggingbox_deref_mut_supports_method_calls_on_inner_vec() {
let mut lb = LoggingBox::new(vec![1, 2, 3]);
lb.push(4); // DerefMut coercion により Vec::push が呼べる
assert_eq!(*lb, vec![1, 2, 3, 4]);
}
}
▶ 実行結果を見る(cargo run / cargo test)
$ cargo run
HELLO
first char via deref: h
Hello, Rust!
[read] value accessed
before: 0
[write] value accessed (mutably)
[read] value accessed
after: 10
$ cargo test
running 3 tests
test tests::loggingbox_deref_mut_actually_mutates_inner_value ... ok
test tests::loggingbox_deref_mut_supports_method_calls_on_inner_vec ... ok
test tests::mybox_deref_coercion_reaches_str_methods ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
🪜 Step-by-Step 解説
Box<String> から &str へのcoercionを追うprint_upper(&boxed)という呼び出しにおいて、boxedの型はBox<String>なので&boxedの型は&Box<String>です。しかしprint_upperは&strを要求しています。コンパイラは次の連鎖を自動的に見つけます。
Box<String>: Deref<Target = String>→&Box<String>は&StringにcoerceできるString: Deref<Target = str>→&Stringは&strにcoerceできる
&*&*boxedのような明示的な変換を一切書く必要がありません。(*boxed)の方は明示的な*演算子で、Deref::deref(&boxed)を呼んで&Stringを取り出したうえで、さらにメソッド呼び出し構文(.chars())自体が自動参照・自動デリファレンスを行うためStringのメソッドがそのまま呼べています。
MyBox<T>にDerefを実装するimpl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
greet(&m)(m: MyBox<String>)を呼んだ瞬間、コンパイラは&MyBox<String>を&strにcoerceする経路を探します。MyBox<String>: Deref<Target = String>(今回実装した分)と、標準ライブラリのString: Deref<Target = str>を連結し、Box<String>の場合とまったく同じ2段階coercionが、自作の型に対しても働きます。これが「Derefを実装するだけで自分の型がスマートポインタとして振る舞える」ことの正体です。
LoggingBox<T>で読み書きの経路を可視化する*counter += 10;という式は脱糖すると*DerefMut::deref_mut(&mut counter) += 10に相当します。つまり+=という複合代入演算子であっても、左辺がCopyでも参照でもない独自型であれば、必ずderef_mutを経由して可変参照を取り出してから加算しています。テストの3つ目(lb.push(4))が特に重要で、メソッド呼び出し構文でも&mut selfを要求するメソッドならDerefMutcoercionが働くことを示しています。Vec::push(&mut self, value: T)は&mut selfを要求するため、lb.push(4)は内部的に(*lb).push(4)相当の展開になり、deref_mutが呼ばれます。
DerefMut: Derefなのか「書き込みできるものは読み込みもできるはず」という直感を型システムに落とし込んだのがこのスーパートレイト境界です。もし
DerefなしでDerefMutだけを実装できてしまうと、*value(不変の文脈でのデリファレンス)が使えないのに*value = x(書き込み)だけができる、という非対称で扱いにくい型が作れてしまいます。実務のコードでは「値を読むためだけに借用したい」場面(例えばprintln!("{}", *value))が非常に多く、書き込み専用の型を許してしまうと、こうした最も基本的な操作すらできない一貫性のない型が量産されるリスクがあります。DerefMut: Derefという制約は、この不自然な状態をコンパイル時にそもそも作れないようにする設計です。
💡 設計思想・なぜこう書くのか
Derefトレイトが体現しているのは、Day 014で学んだAddと同じ「演算子もトレイトメソッド呼び出しに過ぎない」という一貫した設計思想が*演算子にも及んでいるという事実です。Deref coercionは一見「暗黙の型変換」という、通常Rustが避けたがる挙動(暗黙の数値型変換すら許さない言語です)に見えます。しかし、これは無制限な暗黙変換ではなく、「スマートポインタが中身の型に一段近づく方向にのみ」効く、非常に限定されたルールです。&Targetから&Selfに戻る変換や、まったく無関係な型への変換は一切許されません。Box<T>・Rc<T>・String・Vec<T>・MutexGuard<T>のような「中身を包んでいるだけ」の型を、あたかも中身そのものであるかのように扱えるようになります。これは「所有権・借用チェックなどの追加機能を持つラッパーを被せても、利用者側の書き心地を犠牲にしない」というRustの一貫した優先順位を体現した仕組みです。🛑 コンパイルエラーが出た場合
Derefを実装し忘れた型に対して*を使おうとすると、次のようなエラーが出ます。
読み方:
type 'MyBox<i32>' cannot be dereferenced:*演算子を使おうとしたが、MyBox<i32>はDerefを実装していないため、コンパイラは*mをderef呼び出しに展開する方法を知らない、という意味です- 修正方法は
impl Deref for MyBox<T>を追加することです
また、DerefMutだけを実装しDerefを実装し忘れた場合は、次のようなエラーになります。
読み方: DerefMut: Derefというスーパートレイト境界がある以上、Derefを実装していない型にDerefMutだけを実装することはできません。まずDerefを実装してからDerefMutを実装する必要があります。
🌐 他言語との比較
| 観点 | Rust | C++ | Go | Java | TypeScript |
|---|---|---|---|---|---|
| ポインタ風の型に「中身のように振る舞わせる」仕組み | Deref/DerefMutトレイトを実装し、*演算子とcoercionで実現 | operator*・operator->をオーバーロード(unique_ptr/shared_ptrが実装) | 演算子オーバーロード自体が存在しない。代わりに構造体の埋め込み(embedding)でフィールド・メソッドの昇格が自動的に起きる | 演算子オーバーロードなし。ラッパー型の中身を使うには.get()等の明示メソッド呼び出しが必須 | 演算子オーバーロードなし。ラッパー型の中身を使うには明示的なプロパティアクセスが必須 |
| 暗黙の型変換の許容範囲 | Deref coercionのみ極めて限定的に許可(「中身の型に近づく方向」のみ)。数値型の暗黙変換すら禁止という厳格な言語の中の例外的存在 | 暗黙変換に比較的寛容(コンストラクタ経由の暗黙変換等、意図しないバグの温床としてしばしば批判される) | 該当なし(暗黙変換が少ない言語設計) | 該当なし(オートボクシング以外は明示的) | 構造的型付けにより「形が合えば代入可能」という別種の緩さがある |
| 書き込み専用の非対称な型を作れるか | DerefMut: Derefのスーパートレイト境界により作れない(設計上禁止) | operator->だけ実装しoperator*を実装しない、のような非対称な実装は言語上可能(設計判断は実装者に委ねられる) | 該当なし | 該当なし | 該当なし |
Go言語の構造体埋め込みはDerefと目的が似ています(内側の型のフィールド・メソッドをあたかも自分のものであるかのように使える)が、コンパイラが自動で行う「昇格(promotion)」というメカニズムであり、Rustのように明示的なトレイト実装を要求しません。Rustが明示的なimpl Derefを要求するのは、「この型はポインタのように振る舞ってよい」という意図をコード上で宣言させるためであり、暗黙のうちに継承的な関係が生まれることを防ぐ設計判断です。
🏆 実務での使いどころ
- 標準ライブラリの至るところで採用されている:
String: Deref<Target = str>・Vec<T>: Deref<Target = [T]>・Box<T>: Deref<Target = T>・Rc<T>: Deref<Target = T>など、所有権付きの型からその「借用版」の型へ自然にアクセスできるようにする、標準ライブラリ全体を貫く設計パターンです - ロックガード型:
std::sync::MutexGuard<T>はDeref/DerefMutを実装しており、*guardで中身にアクセスできます。ロックの取得・解放(Dropによるアンロック)という重要な副作用を隠しながら、利用者には「ただの値」のように見せる、実務での典型的な応用例です - newtypeパターンでのエルゴノミクス確保: ドメイン型(例:
UserId(String)のようなnewtype)に対してバリデーション付きのラッパーを被せつつ、Derefを実装することで既存の&strメソッド群をそのまま呼べるようにする、というテクニックが実務で使われます(ただし濫用は次の「よくある誤解」で触れる問題を招きます)
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
Derefを実装すればどんな型にも自由に暗黙変換できると思い込む | C++の暗黙変換コンストラクタや、動的型付け言語の緩さに慣れていると「便利な変換機構」に見える | Deref coercionは「ラッパー型からその中身の型へ、一方向にのみ」効く極めて限定的な機構。任意の型同士の変換には使えない(それをやりたいならFrom/Intoトレイトを使うのがRust流) |
newtypeパターンの型に安易にDerefを実装し、まるでOOPの継承のように中身のメソッドを丸ごと透過させてしまう | 「元の型の機能を全部使いたい」という便利さの誘惑に負けやすい | Rust API Guidelines(C-DEREF)では、Derefは「本当にスマートポインタとして中身を透過的に見せたい型」にのみ使うべきとされている。UserId(String)のような検証済みドメイン型にDerefを付けると、バリデーションをすり抜けて生のStringメソッドが使えてしまい、newtypeで得たかった型安全性が骨抜きになる |
DerefMutさえ実装すればDerefは省略できると思う | Derefは「読み取り専用の弱い機能」、DerefMutは「その上位互換」という直感的な理解をしてしまう | DerefMut: Derefというスーパートレイト境界があるため、Derefの実装は必須。省略するとコンパイルエラー(E0277)になる |
🚀 次のステップ
- 発展:
MyBox<T>にDropトレイトも実装し、「値がスコープを抜けるときにログを出す」ことで、Box<T>が単なるDerefだけでなく所有権の破棄タイミングまで管理していることを体感してみてください - 次回予告: Day 016ではIteratorトレイトとイテレータアダプタを扱います。
map/filter/foldなどの連鎖処理が、実際にはどのようなトレイトメソッドの組み合わせで動いているかを学びます