WapLでの簡易的な所有権
所有権と聞いたらまずRust言語を思い浮かべる人が多いでしょう。WapLでもRustの所有権を参考にしてとても簡易的なものではありますが所有権によってメモリリークや多重解放をコンパイル時に検出してエラーを出せます。ただし、WapLではRustの所有権よりもチェックは極めて緩く、例えばメモリ競合や多次元配列のメモリリークなどは防ぐことはできません。また、メモリの確保と解放は自動では行われずC言語のように明示的に記述する必要もあります。とはいえ、明示的に所有者や借用やムーブを記述するため、プログラマはメモリ安全を意識してコーディングができるでしょう。WapLの簡易的な所有権はRustのともまた少し異なるため慣れるまでに少し時間がかかるかもしれませんが、慣れてしまえばガベージコレクションのようなランタイム処理によるオーバーヘッドなしで安全なコードを書くことができます。
所有権規則
所有権のルールは以下の通りです
- ヒープ上にある値に対して所有者であるポインタがただ一つ存在する。
- 所有者はスコープを抜けるまでに解放または所有権の譲渡がされていなければいけない。
変数スコープ
スコープとは、変数が有効な範囲のことです。基本的にWapLでは{}で囲まれた範囲が一つのローカルスコープになります。
fn main():i32{ // aはまだ有効ではない
#=(a, 10, i64); // aが有効になる
loopif:Scope(true){ // sはまだ宣言されていないため有効ではない
#=(s, "hello", ptr:char); // ここでsが有効になる
warpto(break-Scope);
} // スコープが終わりsは無効になる
return 0s; // スコープが終わりaが無効になる
}
コメントで書いたようにスコープを抜けるまではそのスコープの中で宣言された変数は有効で、スコープを抜けたときに無効になります。
メモリ確保
WapLには2種類のメモリ確保があります。必要なサイズのメモリをヒープ領域から動的に取ってくるmallocとメモリをスタック領域に確保するsallocがあります。スタック確保の場合は関数を抜けると自動的に解放され、返り値として関数の外に持ち出すことはできません。一方、ヒープ確保の場合はfreeで解放する必要があり、所有権のチェックの対象です。
fn main():i32{
#=(sz, sizeof(i64), i64); // i64の型のバイト数
#=(h, malloc(*(sz,10),i64), *:i64); // ヒープ確保してi64のポインタに割り当てる
#=(s, salloc(i64,10), ptr:i64); // スタック確保してi64のポインタに割り当てる
free(pmove(h)); // ヒープ確保したhを解放する
return 0s;
}
mallocのときはサイズは変数でいいですが、sallocのときはサイズがi64リテラルである必要があります。また、ここでそれぞれ型名が*:i64とptr:i64で異なる理由は次に説明します。
4種類のポインタ型
第2章の基本的な型で少し触れたとおりWapLには4種類のポインタがあります:ptr:T *:T &:T &mut:Tです。
| 型 | 説明 |
|---|---|
| ptr | 所有権のチェックを一切受けず、スタック上のメモリやより自由な操作がしたいときの用いる |
| * | ヒープ確保される所有者ポインタで、値に対してただ一つのみ存在し、スコープを抜けるまでにpmoveで譲渡される必要がある |
| & | 不変借用ポインタで値の書き変えとpmoveが禁止されている |
| &mut | 可変借用ポインタで値の書き変えはできるがpmoveは禁止されている |
fn main():i32{
#=(sz, sizeof(i64), i64)
#=(owner, malloc(*(sz,10),i64), *:i64); // 所有者
=(owner, Array(1,2,3,4,5,6,7,8,9,0)); // 値を入れる
#=(immut, p&(owner), &:i64); // 不変借用
#=(mutable, p&mut(owner), &mut:i64); // 可変借用
=([](mutable,5),10); // &mutは書き変え可能
#=(i,0,i64)
loopif:(<(i,10)){
println(format("%d",[](owner,i)));
=(i,+(i,1));
}
#=(owner2, pmove(owner), *:i64); // 所有権をownerからowner2に譲渡し以降ownerは使えない
free(pmove(owner2)); // 所有者を解放
return 0s;
}
所有権の譲渡:ムーブ
先ほどのポインタ型の説明での#=(owner2, pmove(owner), *:i64)のように所有権を譲渡した後、前の所有者は無効になり、アクセスしようとするとエラーが出ます。
fn main():i32{
#=(sz, sizeof(i64), i64)
#=(owner, malloc(*(sz,10),i64), *:i64); // 所有者
#=(owner2, pmove(owner), *:i64); // 所有権をownerからowner2に譲渡し以降ownerは使えない
free(pmove(owner)); // 前の所有者を解放しようとしているためエラー
free(pmove(owner2));
return 0s;
}
Error:"owner" already moved. it is prohibited to read moved pointer
Error : at pmove "owner" already moved
これにより二重解放を防ぐことができます。
所有者の責任
所有者はスコープを抜けるまでに所有権を譲渡するか解放する必要があります。
fn main():i32{
#=(sz, sizeof(i64), i64)
#=(p, malloc(*(sz,10),i64), *:i64); // 所有者
return 0s; // まだ譲渡も解放もしてない
}
Error:you need to free or drop pointer p!
このようにエラーが出ることでメモリリークを防げます。
借用と参照
先ほどのポインタ型の説明でもあったようにWapLではp&やp&mutで同じ場所を指す所有権を持たないポインタを作ることができます。WapLではこれを借用と呼んでいます。Rustでは関数の引数に参照を取ることを借用と呼んでいますがWapLでは少し異なり、借用はポインタから作ります。WapLにもRustのように&_(値を持つ変数)で参照を生成することもでき、これはその変数の値が格納されてるアドレスを指すポインタを返しています。また、実際はp&やp&mutはほとんど何もしておらず、受け取る側の型によってそのポインタをどのポインタ型として扱うかを決定しており、どのような形でポインタを渡しているのかを明示的に書くようにしているだけです。
参照の逆は参照外しであり、*_(ポインタ変数)または*_(ポインタ変数,返す型)のように記述します。
fn get_at(&:i64 arr,&mut:i64 i):i64{
=(*_(i), -(*_(i),1));
return [](arr, *_(i));
}
fn main():i32{
#=(sz,sizeof(i64),i64);
#=(array, malloc(*(sz,5),i64), *:i64);
=(array, Array(3,1,4,1,5));
#=(indx, 2, i64);
println( format("[](array,-(2,1)) = %d\nindex = %d",get_at( p&(array), &_(indx) ), indx ) );
free(pmove(array));
return 0s;
}
[](array,-(2,1)) = 1
index = 1
このように&:i64で渡すことで所有権を譲渡せずに参照を渡すことができます。また、indxは参照渡しでget_at関数に渡されているため、そこでの値の変化がmain関数にも反映されています。