第10章 値・所有権・ライフサイクル

10.1 この章で学ぶこと

この章で学ぶ内容は以下のとおりです。

表10-1: この章で学ぶこと

学ぶこと

ポイント

引数修飾子の選び方

immmutvarref のどれを使うか

所有権がアセンブリに現れる様子

__init__ → 使用 → refcount 0 → __deinit__ の流れ

ライフタイムとライフサイクルの違い

参照の有効期間 vs 値全体の一生

Rust との設計の差

Origin(Mojo)と lifetime parameter(Rust)の違い

10.2 Values:値とメモリ配置

Mojo の値はスタックまたはヒープに置かれます。 どちらに置かれるかはコンパイル時に確定し、実行時に切り替わることはありません。

表10-2: Values:値とメモリ配置

型の特性

配置

サイズが固定・小さい

スタック(大半の struct

IntFloat64Counter

サイズが可変または大きい

ヒープ(ポインタ経由)

List[T]String(データ本体)

参照カウント対象

ヒープ(ArcPointer 等)

複数の所有者が必要な場合

def main():
    var n: Int = 40
    var xs: List[Int] = [n, n + 1, n + 2]
    print(n, len(xs))

リスト10-1: values_int_and_list.mojo

各変数のメモリ配置を追います。

var n: Int = 40 は 8 バイトの整数をスタック上に確保します。 コンパイル時にサイズが確定しているため、関数プロローグ(subq $..., %rsp)で main() 全体の領域がまとめて確保されます。 実行時の動的確保は一切発生しません。

var xs: List[Int] = [n, n + 1, n + 2] では xs 自体はスタック上のヘッダ(ポインタ、長さ、キャパシティの 3 フィールド)として配置されますが、要素 3 個分(24 バイト)の配列データはヒープ上に確保されます。 xs がスコープを抜けると List のデストラクタがヒープ領域を自動解放します。

「ヘッダはスタック、データはヒープ」というパターンは String も同じ設計です。 固定サイズのヘッダをスタックに置くことで、関数呼び出しのオーバーヘッドを最小にしながら可変長データを扱えます。

Values:値とメモリ配置(スタック配置とヒープ配置の対比)

図10-1: スタック配置(IntCounterFloat64)とヒープ配置(List[Int]StringArcPointer[T])の対比

左側がスタック配置、右側がヒープ配置です。スタック上の変数はアドレス(0x10(%rsp) 等)が関数プロローグで一括確保されます。List[Int]String はヘッダ(ptr、length、capacity)のみスタックに置き、実データはヒープに確保します。ArcPointer[T] は参照カウント(RC=1)付きでヒープ上のオブジェクトを共有します。

10.3 Value semantics:値の受け渡し

Mojo は value semantics(値セマンティクス)が基本です。 変数は「オブジェクトへの参照」ではなく「値そのもの」を保持します。 代入すれば独立したコピーが生まれ、一方を変えてももう一方には影響しません。

表10-3: Value semantics:値の受け渡し

Python

Mojo

x = y の意味

同じオブジェクトを指すポインタのコピー

論理的に独立した値(実際はムーブで最適化)

関数に渡す

参照を共有(呼び出し先が変更できる)

デフォルトは imm(読み取り専用の借用)

変更したい場合

そのまま可能(副作用が暗黙)

mut を明示する必要がある

struct Counter:
    var n: Int

    def __init__(out self, start: Int):
        self.n = start

    def get(self) -> Int:
        return self.n

    def bump(mut self):
        self.n += 1


def show(c: Counter):
    print(c.get())


def main():
    var c = Counter(0)
    show(c)
    c.bump()
    show(c)

リスト10-2: value_semantics_counter.mojo

コードの各部分を追います。

def show(c: Counter):c には引数修飾子がありません。 省略時は imm(読み取り専用の借用)として扱われます。 show の中で c.bump() のような変更メソッドを呼ぼうとするとコンパイルエラーになります。 呼び出し元の c はそのまま保たれます。

c.bump()mut self を持つメソッドです。 c のアドレスをメソッドに渡して n を直接書き換えます。 最初の show(c)0c.bump() の後の show(c)1 を表示します。

Python では show(c) 内で c.n = 99 のような変更が可能です(引数が参照共有のため)。 Mojo では imm で渡された値への変更はコンパイル時に拒否されます。 「この関数が引数を変更するかどうか」がシグネチャを見るだけで判断できます。

Python の参照セマンティクスと Mojo の値セマンティクスの比較

図10-2: Python(参照セマンティクス)と Mojo(値セマンティクス)の比較

左側の Python では y = x で同じオブジェクトを共有するため、y.value = 99x.value にも影響します。右側の Mojo では var y = x で独立したコピー(またはムーブ)が生まれ、y.bump()x に影響しません。関数呼び出しも imm(読み取り専用借用)がデフォルトで、変更したい場合は mut を明示します。

10.4 Ownership:引数修飾子の使い分け

10.4.1 4 つの修飾子

引数の受け渡し方を制御する 4 つの修飾子をまとめます。

表10-4: 4 つの修飾子

修飾子

意味

元の値への影響

C++ での類比

imm(省略可)

読み取り専用の借用

変わらない

f(const T&):const 参照。コピーなしで読み取れる

mut

変更可能な借用

変わる(直接書き換え)

f(T&):非 const 参照。呼び出し元の値を直接書き換える

var

ローカルコピーを持つ

変わらない(コピーに操作)

f(T):値渡し。関数内で独立したコピーを持つ

ref

借用を受け取る

mut と同様

f(T&) に近いが、ライフタイム情報も伴う

「C++ での類比」は C++ の引数渡し規約に対応したアナロジーです。 表中の f は「任意の関数名」を表す仮置きの識別子です。 C++ の const T& は C++ 固有の記法で、const(変更不可)と &(参照)を意味します。

10.4.2 フローチャート:どれを選ぶか

実際のコードを書くとき、「どの修飾子を使えばよいか」は 2 つの問いに答えるだけで決まります。 「呼び出し元の値を変更するか」と「所有権を引き継ぐか」です。

引数修飾子の選び方フローチャート

図10-3: 引数修飾子の選び方フローチャート

10.4.3 imm / mut / var の実例

3 つの修飾子が 1 つのファイルに凝縮されたサンプルです。 imm による借用、ref によるインプレース書き換え、^ による所有権転送を順に確認します。

def sum_borrow(imm xs: List[Int]) -> Int:
    var s = 0
    for x in xs:
        s += x
    return s


def take_owned(var xs: List[Int]):
    print(String("took len="), len(xs))


def main():
    var xs: List[Int] = [1, 2, 3]
    print(sum_borrow(xs))
    for ref x in xs:
        x *= 10
    print(sum_borrow(xs))

    var data: List[Int] = [7, 8]
    take_owned(data^)

リスト10-3: ownership_read_ref_and_move.mojo

表10-5: imm / mut / var の実例

コードの箇所

何が起きているか

sum_borrow(imm xs: List[Int])

xs の所有権は呼び出し元のまま。関数内で変更不可

for ref x in xs: x *= 10

ref で各要素への可変参照を得る。xs の中身を直接書き換える

take_owned(data^)

^data の所有権を take_owned に転送。この後 data は使えない

コードの動きを順に追います。

sum_borrow(imm xs)imm xsxs を読み取り専用で借用します。 xs の所有権は main のままです。 List 全体をコピーするコストがなく、元のデータを安全に参照できます。 関数内で xs.append(...) のような変更を試みるとコンパイルエラーになります。 1 回目の呼び出しは 6(1+2+3)を返します。

for ref x in xs: x *= 10ref xxs[i] のアドレスへのエイリアスです。 x *= 10 は元のリストを直接書き換えます。 このループ後、xs[10, 20, 30] になります。 2 回目の sum_borrow(xs)60 を返します。

take_owned(data^)^ は所有権転送演算子(transfer operator)です。 data の所有権を take_owned に渡します。 この行以降 data を参照するとコンパイルエラーになります。 受け取った var xstake_owned のローカル変数として扱われ、関数を抜けるとデストラクタが走ってヒープが解放されます。 呼び出し元で free を書く必要はありません。

10.5 アセンブリで見る lifecycle

Mojo の「スコープを抜けたら自動で解放される」という動作は、ソースコードの書き方とは無関係にコンパイラが __deinit__ 呼び出しを挿入することで実現されています。 このセクションでは実際に生成されるアセンブリを見て、その仕組みを確認します。

lifecycle_resource_traces.mojo のソースを確認してからアセンブリを読みます。

struct Resource:
    var id: Int

    def __init__(out self, id: Int):
        self.id = id
        print("init", id)

    def __deinit__(deinit self):
        print("del", self.id)


def main():
    var r = Resource(1)
    print(r.id)

リスト10-4: lifecycle_resource_traces.mojo

このコードのアセンブリを main() の先頭から末尾まで読みます。 実際のアセンブリ出力は標準ライブラリの実装を含め数百行になりますが、ここでは main() に相当する箇所を抜粋します。

STEP 1:スタック確保とコンストラクタ呼び出しvar r = Resource(1)

; def main():
   590:  subq   $0x78, %rsp          ; スタックフレーム確保(120 バイト)

; var r = Resource(1)
   594:  movl   $0x1, %edi           ; 引数 id = 1 をセット
   599:  leaq   0x10(%rsp), %rsi     ; self(r)のスタック上アドレスをセット
   59e:  callq  "_lifecycle_resource_traces::Resource::__init__(::SIMD[::DType(int), ::SIMDLength(1)])"   ; ← コンストラクタ呼び出し

r はスタックの 0x10(%rsp) に直接置かれます。 ヒープ確保はありません。 コンストラクタのシンボル名にモジュール名プレフィックス(_lifecycle_resource_traces::)が付き、Int の実体である SIMD[DType.index, 1] がそのまま展開された形で表示されているのが 1.0 での見た目の違いです(IntSIMD のエイリアスであることが、デマングル結果により忠実に反映されています)。

STEP 2:print(r.id) の実行

; print(r.id)
   5a5:  leaq   _static_string_a8d4ace0dc8d360e(%rip), %rax   ; sep(空文字)の先頭アドレス
   5ac:  movq   %rax, 0x38(%rsp)
   5b1:  movq   $0x1, 0x40(%rsp)
   5ba:  movq   $0x1, 0x48(%rsp)
   5c3:  movq   %rax, 0x18(%rsp)
   5c8:  movq   %rax, 0x20(%rsp)
   5cd:  movq   %rax, 0x50(%rsp)
   5d2:  movq   %rax, 0x58(%rsp)
   5d7:  movq   $0x1, 0x60(%rsp)     ; ← ここまでで sep の StringSpan をスタック上に組み立てる

   5e2:  leaq   _static_string_bbe01a6a523daf15(%rip), %rax   ; end(改行)の先頭アドレス
   5e9:  movq   %rax, 0x38(%rsp)
   5ee:  movq   $0x1, 0x40(%rsp)
   5f7:  movq   $0x1, 0x48(%rsp)
   600:  leaq   _static_string_bbe01a6a523daf15(%rip), %rax
   607:  movq   %rax, 0x28(%rsp)
   60c:  leaq   _static_string_bbe01a6a523daf15(%rip), %rax
   613:  movq   %rax, 0x30(%rsp)
   618:  leaq   _static_string_bbe01a6a523daf15(%rip), %rax
   61f:  movq   %rax, 0x50(%rsp)
   624:  leaq   _static_string_bbe01a6a523daf15(%rip), %rax
   62b:  movq   %rax, 0x68(%rsp)
   630:  movq   $0x1, 0x70(%rsp)     ; ← ここまでで end の StringSpan をスタック上に組み立てる

   639:  movq   0x10(%rsp), %rdi     ; r.id(Int)を第 1 引数にロード
   63e:  movq   %rsp, %rax
   641:  movq   $0x1, (%rax)         ; 引数カウント = 1
   648:  leaq   _static_string_a8d4ace0dc8d360e(%rip), %rsi   ; sep
   64f:  leaq   _static_string_bbe01a6a523daf15(%rip), %rcx   ; end
   656:  movl   $0x1, %r8d
   65c:  xorl   %r9d, %r9d
   65f:  movq   %r8, %rdx
   662:  callq  "_std::io::io::print[..., Int]"    ; ← print 実行

文字列リテラルは .rodata の静的データへの参照であり、ヒープ確保も refcount 管理も発生しません。 ただし 1.0 では sep(区切り文字)と end(末尾文字)が単なるポインタではなく、長さやフラグを含む StringSpan 構造体としてスタック上に明示的に組み立てられてから print に渡されるようになりました。 そのため命令数は増えていますが、「静的データへの参照を組み立てて渡す」という骨格自体は変わっていません。

STEP 3:__deinit__ の自動挿入r のスコープ終了)

; r のスコープが終わる(main の末尾)
   667:  leaq   0x10(%rsp), %rdi     ; r のアドレスを第 1 引数にセット
   66c:  callq  "_lifecycle_resource_traces::Resource::__deinit__(lifecycle_resource_traces::Resource$)"  ; ← デストラクタ(自動挿入)

STEP 4:スタックフレーム解放と return

; def main():
   671:  addq   $0x78, %rsp          ; スタックフレームを解放(STEP 1 の逆)
   675:  retq                         ; return

ソースコードに r.__deinit__() は存在しません。 コンパイラが r のスコープ終了を検出して 0x66c に挿入しています。 シンボル名末尾の $ は Mojo の “consuming” 呼び出し(所有権を消費する呼び出し)を表します。

../_images/assembly_lifecycle_steps.jpg

図10-4: アセンブリで見る lifecycle(STEP 1〜4 の全体像)

コラム: RAII(資源の寿命を型に結びつける)

RAII(Resource Acquisition Is Initialization)は、C++ が発祥のパターンです。 「資源の取得(open/alloc など)をコンストラクタに、解放(close/free など)をデストラクタに書く」という設計です。

Mojo の __init__ / __deinit__ も同じ発想です。 スコープを抜けた瞬間にコンパイラが __deinit__ を呼ぶため、次の利点があります。

  • 解放忘れが起きない

  • 例外や早期 return があっても確実に解放される

Python の with ブロック(__enter__ / __exit__)は同じ問題を解決しますが、「使う前に with を書く」というユーザーの義務があります。 RAII は型のスコープと解放が自動的に対応します。

10.6 Lifetimes:参照の有効期間

ライフタイムは参照が安全に使える期間です。 imm xs で借用した参照は、元の xs が生きている間だけ使えます。 xs が解放された後も参照を使い続けようとすること(ダングリング参照)は、Mojo の設計上はコンパイル時に検出して拒否されます(静的検査の成熟度については後述します)。

def first_len(imm xs: List[Int]) -> Int:
    return len(xs)


def main():
    var buf: List[Int] = [1, 2, 3, 4]
    print(first_len(buf))

リスト10-5: lifetimes_read_param.mojo

first_len(imm xs: List[Int])imm xs は、xs の所有権を受け取らず参照のみを借用します。 関数内で xs のデータを参照できますが、xs は呼び出し元が所有し続けます。 関数が返ると借用も終わります。

ダングリング参照の例を示します。

# 概念的な例:Mojo の設計では NG
def get_ref_bad() -> ref [MutAnyOrigin] Int:
    var x: Int = 42
    return x   # x はこの関数のスコープで消滅する → ダングリング参照

[MutAnyOrigin] は「任意の可変 origin」を意味します。 xget_ref_bad のスタックフレームに存在するローカル変数で、関数が返るとスタックフレームごと消滅します。 Mojo の設計では、origin がこの関数のスコープを越えられないことをコンパイラが検出してエラーにします。 返った先の呼び出し元が受け取る参照は存在しないメモリを指すことになるためです。

注釈

Mojo 1.0.0 ではダングリング参照の静的チェックが未完全で、上記のコードはコンパイルエラーにならないケースがあります。将来バージョンで完全に実装される予定です。

表10-6: Lifetimes:参照の有効期間

規則

内容

参照の有効期間

参照する元の値が生きている間だけ使える

ダングリング参照

解放された値を指す参照。Mojo は設計上コンパイル時に拒否する(1.0.0 では静的検査が未完成)

Origin

Mojo がライフタイムを追跡するための内部概念。Rust の lifetime parameter に相当

コラム: Rust のライフタイムとの違い

Rust では fn f<'a>(x: &'a T) -> &'a T のように、ライフタイム名('a)を明示的に書く必要があります。

Mojo では同じ概念を Origin として内部で扱い、多くの場合ユーザーが明示しなくても推論されます(1.0 時点では一部の複雑なケースで明示が必要)。

これは Mojo が「Python に近い書きやすさ」と「所有権の安全性」を両立するための設計的な選択です。 Swift の ARC(自動参照カウント)よりは明示的で、Rust より緩やかな中間地点を目指しています。

ダングリング参照にならない 2 つのパターンを示します。

パターン 1:引数の origin を受け継ぐ(最も慣用的)

def first(ref list: List[Int]) -> ref [origin_of(list[0])] Int:
    return list[0]   # list は呼び出し元が所有 → スコープを越えられる

def main():
    var xs: List[Int] = [1, 2, 3]
    var r = first(xs)   # r は xs が生きている間だけ有効
    print(r)            # 1

リスト10-6: ref_return_origin.mojo

list は呼び出し元(main)が所有するため、main が終わるまで生きています。 ref [origin_of(list[0])] と書くことで「この参照の寿命は list の寿命と同じ」とコンパイラに伝えます。

注釈

Mojo 1.0 では Interior Origins が導入され、ListDictString などのコンテナが返す要素への参照は、コンテナ全体の origin ではなく要素固有の origin(interior origin)として追跡されるようになりました。 そのため origin_of(list)(コンテナ全体)ではなく origin_of(list[0])(要素)と書く必要があります。 これにより、要素への参照を保持したままコンテナを変更するコード(例:参照を握ったまま list.append(...) する)をコンパイラがより正確に検出できるようになりました。

パターン 2:所有権を転送して返す

List[T] はヒープ上にデータを保持します。 ^ 演算子で所有権ごと呼び出し元に転送すれば、スタックスコープに縛られず安全に返せます。

def make_heap_int() -> List[Int]:
    var data = List[Int]()
    data.append(42)
    return data^   # 所有権ごと呼び出し元に転送

def main():
    var data = make_heap_int()
    print(data[0])   # 42

リスト10-7: move_transfer.mojo

data^^ は所有権の転送(move)を示します。 List[Int] はヒープバッファを内部に持ち、そのバッファの所有権が呼び出し元に移ります。 関数のスタックフレームが消えても、ヒープ上のデータは呼び出し元が所有するため安全です。

表10-6b: ダングリング参照にならない条件

パターン

有効期間

返す型

ローカル変数を返す

関数終了で死ぬ → NG

引数の origin を受け継ぐ

呼び出し元のスコープ

ref [origin_of(...)] T

所有権転送

呼び出し元のスコープが終わるまで

List[T]^ で転送)

../_images/lifetimes_origin_summary.jpg

図10-5: Lifetimes:参照の有効期間と Origin の役割まとめ

10.7 Lifecycle:値の一生

10.6 節で扱ったライフタイムとこの節で扱うライフサイクルは、見た目が似ていますが対象がまったく異なります。

ライフタイム(Lifetime) は「参照がいつまで安全に使えるか」という話です。 Origin が追跡し、参照元の値が消えた後も参照を使おうとするとコンパイルエラーになります。 参照自体はデータを持たず、別の値を指しているだけです。

ライフサイクル(Lifecycle) は「値そのものがどう生まれて、どう消えるか」という話です。 __init__ で生まれ、使われ、スコープを抜けると __deinit__ が呼ばれて消えます。 値がリソース(ヒープ、ファイルハンドルなど)を持つ場合、その確保と解放がここに集約されます。

../_images/lifecycle_value_life.jpg

図10-7: Lifecycle:値の一生(誕生・使用・解放とライフタイムとの関係)

参照(p)のライフタイムは、参照元(r)のライフサイクルの中に収まらなければなりません。これがライフタイムとライフサイクルの関係です。

表10-7: ライフタイムとライフサイクルの違い

ライフタイム

ライフサイクル

対象

参照(借用)

値そのもの

期間

参照元が生きている間

__init__ から __deinit__ まで

問題

ダングリング参照

リソースリーク・二重解放

解決策

Origin による追跡

__init__ / __deinit__ / RAII

関連する修飾子

immrefmut

var^(transfer)

10.8 Life:コピーとムーブの入り口

コピーとムーブの動作

値を別の変数に渡すとき、Mojo は「コピー」と「ムーブ」の 2 つの方法を区別します。

コピー はデータの複製を作ります。 元の変数も新しい変数も、それぞれ独立したデータを持ちます。

var b = a.copy()

a [data: "hello"]   ← そのまま残る
b [data: "hello"]   ← 新しいコピー(独立)

ムーブ は所有権を転送します。 元の変数は消滅し、データはコピーされません。

var b = a^

a (消滅)
b [data: "hello"]   ← a が持っていたデータをそのまま引き継ぐ

コピーはデータを丸ごと複製するコストがかかります。 ムーブは「誰が所有者か」という情報だけを更新するため、データサイズに関わらず低コストです。

../_images/life_copy_vs_move.jpg

図10-6: Life:コピーとムーブの違い

10.8.1 コピーの実装:Copyable__copyinit__

Copyable trait を実装した型は a.copy() でコピーを作れます。 @fieldwise_init と組み合わせると __init____copyinit__ が自動生成されます。

@fieldwise_init
struct Label(Copyable):
    var text: String


def main():
    var a = Label("hi")
    var b = a.copy()
    print(a.text, b.text)

リスト10-8: life_copyable_label.mojo

a.copy() を呼ぶと __copyinit__ が実行され、a とは独立した b が作られます。 abmain() を抜けるまで生き続け、それぞれのスコープ終了時に __deinit__ が呼ばれます。

__copyinit__ が呼ばれる主なタイミングは次の 2 つです。

  1. a.copy() を明示的に呼ぶとき

  2. var 引数で関数に渡すとき(value semantics による暗黙コピー)

10.8.2 ムーブの実装:Movable^

Movable trait を実装した型は ^ 演算子(transfer operator)で所有権を転送できます。

@fieldwise_init
struct Buffer(Movable):
    var size: Int

    def __deinit__(deinit self):
        print("del", self.size)


def main():
    var a = Buffer(size=3)
    print("a.size =", a.size)
    var b = a^          # a の所有権が b に移る(a は消滅)
    print("b.size =", b.size)
    # a.size  ← コンパイルエラー: a はすでに消滅している

リスト10-9: life_movable_buffer.mojo

var b = a^a の所有権が b に移ります。 この時点で a は消滅し、以降 a にアクセスするとコンパイルエラー(use of uninitialized value 'a')になります。 __deinit__ はムーブ先の bmain() を抜けるときに 1 回だけ呼ばれます。 a__deinit__ は呼ばれません。

@fieldwise_initMovable を組み合わせると、__moveinit__(ムーブコンストラクタ)が自動生成されます。 これは各フィールドを順にムーブして新しいインスタンスを初期化します。

表10-8: コピーとムーブの比較

操作

元の変数

コスト・trait

コピー(a.copy()

そのまま残る

データ複製 / Copyable

ムーブ(a^

消滅(使用不可)

低コスト / Movable

Copyable のみの型は ^ でムーブできません(コンパイルエラー)。 Movable のみの型は a.copy() でコピーできません。 両方を実装した型はどちらの操作も使えます。

10.9 Initialization:コンストラクタ設計

__init__ は型の初期状態を確定する唯一の場所です。 この節では、__init__ を手動で書くことで「型が常に正しい状態でしか存在できない」ように設計する方法を示します。

@fieldwise_init と手動 __init__ の違い

@fieldwise_init はフィールドをそのまま引数に取る __init__ を自動生成します。 手軽ですが、呼び出し元がすべてのフィールドを自由に指定できるため、意図しない状態を外から作られるリスクがあります。

@fieldwise_init
struct IntRange:
    var lo: Int
    var hi: Int


def main():
    # 以下は両方コンパイルが通る
    var ok  = IntRange(lo=8,  hi=12)   # lo <= hi → 正常
    var bad = IntRange(lo=12, hi=8)    # lo > hi  → 不正な状態
    print(ok.lo, ok.hi)
    print(bad.lo, bad.hi)

リスト10-10: fieldwise_init_intrange.mojo

IntRange が「整数の区間」を表す型なら、lo <= hi という条件(不変条件、invariant)は常に満たされるべきです。 @fieldwise_init ではこれを強制できません。

コンストラクタで不変条件を保証する

手動で __init__ を書き、呼び出し元から意味のある引数だけを受け取るよう設計します。

struct IntRange:
    var lo: Int
    var hi: Int

    def __init__(out self, center: Int, width: Int):
        self.lo = center - width // 2
        self.hi = center + width // 2


def main():
    var r = IntRange(10, 4)
    print(r.lo, r.hi)

リスト10-11: init_box_bounds.mojo

center(中心値)と width(幅)だけを受け取り、lohi は内部で計算します。

  • lo = center - width // 2

  • hi = center + width // 2

center=10, width=4 を渡すと lo=8, hi=12(出力 8 12)になります。 lo <= hi は計算上必ず成立するため、外から逆転した区間を作る手段がありません。 lohi のフィールドを直接指定するコンストラクタが存在しないので、「型の外から不正な状態を作れない」構造になっています。

このパターンを不変条件を持つ型と呼びます。 呼び出し元が「値が正しいか確認してから使う」のではなく、「型が正しい状態しか作れない」ように設計することで、バグの混入口をコンストラクタ 1 箇所に集約します。

警告

Mojo 1.0.0 の制限:フィールドのアクセス制御がない

Mojo 1.0.0 には private などのフィールドアクセス修飾子がありません。 var フィールドはすべて外から直接書き込めるため、v.lo = 99 のようにすると不変条件を壊せてしまいます。

__init__ による設計は「正しくない状態のインスタンスを 生成できなく する」効果はありますが、生成後のフィールド直接書き換えまでは防げません。 フィールドを完全に隠蔽するアクセス制御は将来のバージョンで導入される予定です。 現状はフィールド名にアンダースコア(_lo, _hi)を付けて「直接触らない」という慣習で対処することになります。

バリデーション付き __init__

raises を追加すると、不正な入力をコンストラクタで弾けます。

def __init__(out self, center: Int, width: Int) raises:
    if width < 0:
        raise Error("width must be non-negative")
    self.lo = center - width // 2
    self.hi = center + width // 2

こうすると width < 0 の場合は構築自体が失敗し、不正な IntRange が存在する余地がなくなります。

../_images/initialization_constructor_design.jpg

図10-8: Initialization:コンストラクタ設計と不変条件

10.10 Death:デストラクタとヒープ解放

__deinit__ はスコープを抜けるときにコンパイラが自動で呼ぶデストラクタです。 この節では、ヒープ上にデータを持つ型を RAII パターンで安全に管理する実例を通じて、__deinit__ の役割と二重解放防止の仕組みを示します。

HeapInts の設計

HeapInts はヒープ上に Int の配列を確保する型です。 コンストラクタで確保し、デストラクタで解放します。

各フィールドと操作の役割を先に整理します。

フィールド / メソッド

役割

data: Pointer[Int, ...]

ヒープ上の Int 配列を指す生ポインタ

size: Int

配列の要素数

unsafe_alloc[Int](size)

size 個の Int 分のヒープ領域を確保

.unsafe_offset(i)

i 番目の要素を指すようにポインタを移動

.unsafe_write(v)

ポインタが指す場所に値 v を書き込んで初期化

.unsafe_deinit_pointee()

ポインタが指す要素を破棄(__deinit__ を呼ぶ)

.unsafe_free()

ヒープ領域をOSに返却

注釈

Mojo 1.0 では UnsafePointer 型は Pointer 型に統合されました。 「安全でない操作」は型そのものではなく、unsafe_offsetunsafe_writeunsafe_deinit_pointeeunsafe_free のように メソッド名の unsafe_ 接頭辞で明示する設計になっています。 alloc[T](n) も同様に非推奨となり、本書では移行策として提供されている unsafe_alloc[T](n)std.memory.alloc からインポート)を使います。

from std.memory.alloc import unsafe_alloc


struct HeapInts:
    var data: Pointer[Int, MutUntrackedOrigin]
    var size: Int

    def __init__(out self, *values: Int):
        self.size = len(values)
        self.data = unsafe_alloc[Int](self.size)
        for i in range(self.size):
            self.data.unsafe_offset(i).unsafe_write(values[i])

    def __deinit__(deinit self):
        for i in range(self.size):
            self.data.unsafe_offset(i).unsafe_deinit_pointee()
        self.data.unsafe_free()

    def at(self, i: Int) -> Int:
        return self.data.unsafe_offset(i)[]


def main():
    var h = HeapInts(1, 2, 3)
    print(h.at(0), h.at(2))

リスト10-12: death_heap_buffer.mojo

HeapInts(1, 2, 3) を構築すると __init__unsafe_alloc[Int](3) でヒープを確保し、各スロットに unsafe_offset(i).unsafe_write(v) で値を書き込みます。 h.at(0)self.data.unsafe_offset(i)[]i 番目の要素を読み出します。 main() を抜けて h のスコープが終わると __deinit__ が走り、各要素を unsafe_deinit_pointee() で破棄してから unsafe_free() でヒープを返却します。

deinit self の意味

__deinit__(deinit self)deinit は「このメソッドが返った後、self は消滅する」という宣言です。 __deinit__(inout self) とは書きません。 コンパイラは __deinit__ 呼び出し後に self を参照するコードを許可しません。

二重解放を防ぐ仕組み

ヒープ解放で最も危険なバグの 1 つが二重解放(double free)です。 同じメモリを 2 回 free すると、ヒープが壊れて未定義動作を引き起こします。 Mojo は 2 つの規則でこれを構造的に防ぎます。

  1. ^(ムーブ)後は元の変数が使えない:所有権を転送した変数にアクセスするとコンパイルエラー(use of uninitialized value)になります。

  2. __deinit__ はムーブ先が 1 回だけ呼ぶ:値がムーブされた場合、__deinit__ を呼ぶ責任はムーブ先に移ります。ムーブ元の __deinit__ は呼ばれないため、同じバッファが 2 回 free されることはありません。

Python の __del__ との違い

Python の __del__ は GC が呼ぶため「いつ呼ばれるかわからない」問題があります。 ファイルハンドルやソケットを保持する型でも、__del__ がいつ閉じるか不明なため、with 文で明示管理するのが慣例です。

Mojo では __deinit__ はスコープ終了時に確定的に呼ばれます。 ヒープバッファを持つ型を書く場合、__deinit__ に解放処理を書くだけで、呼び出し元に解放の責任を押しつけずに済みます。

../_images/death_destructor_heap.jpg

図10-9: Death:デストラクタとヒープ解放

10.11 まとめ

本章で扱った内容を以下にまとめます。

表10-9: まとめ

要素

押さえるべきこと

imm(引数)

読み取りのみ。元の値は変わらない。デフォルト

mut(引数)

変更可能な借用。元の値を直接書き換える

var(引数)

コピーまたはムーブして受け取る。独立した所有者

^(transfer)

所有権を渡し切る。この後元の変数は使えない

ライフタイム

参照が安全に使える期間。コンパイラが Origin で追跡

ライフサイクル

値全体の一生。__init__ → 使用 → __deinit__

__deinit__ とアセンブリ

callq "Resource::__deinit__(Resource$)" がスコープ終了時に挿入される