はじめに
最近、emscriptenのval.hのRustラッパーを書いていたのですが、Rustのgenerics周りを触っていて少し引っかかったところがあり、Qiitaに集うRustaceanの皆さんにも聞いてほしくて筆を取りました。
サンプルコード
fn foo<T>() {
use std::sync::{Once, ONCE_INIT};
static INIT: Once = ONCE_INIT;
INIT.call_once(|| {
// run initialization here
println!("Called");
});
}
fn main() {
foo::<i64>();
foo::<i64>();
foo::<isize>();
}
このとき、println!
は何回呼ばれるでしょうか?実は、一回しか呼ばれません。
どう気持ち悪いか
なぜわたしはこれを気持ち悪いと思ったのでしょう?C++のテンプレートと同様に、RustのGeneric Functionも、実際に与えられた型T
に応じて異なる「インスタンス」が作られます。各インスタンスは適当にマングリングされたシンボル名を持ち、それぞれ別な関数として機械語に翻訳される、というのがわたしの理解です。
実際、objdumpでアセンブラを見てみると
0000000000007510 <_ZN4test4main17h916a53db53ad90a1E>:
7510: 50 push rax
7511: e8 5a ff ff ff call 7470 <_ZN4test3foo17h61cadb66d9062e2eE>
7516: e8 55 ff ff ff call 7470 <_ZN4test3foo17h61cadb66d9062e2eE>
751b: e8 30 ff ff ff call 7450 <_ZN4test3foo17h09b087b10e1d5121E>
7520: 58 pop rax
7521: c3 ret
7522: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
7529: 00 00 00
752c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
...
0000000000007470 <_ZN4test3foo17h61cadb66d9062e2eE>:
7470: 50 push rax
7471: 48 8d 05 f0 8f 24 00 lea rax,[rip+0x248ff0] # 250468 <_ZN4test3foo4INIT17h68924fbac7fffb27E>
7478: 48 89 c7 mov rdi,rax
747b: e8 b0 f8 ff ff call 6d30 <_ZN3std4sync4once4Once9call_once17h651e8a9fc00134c3E>
7480: 58 pop rax
7481: c3 ret
7482: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
7489: 00 00 00
748c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
...
0000000000007450 <_ZN4test3foo17h09b087b10e1d5121E>:
7450: 50 push rax
7451: 48 8d 05 10 90 24 00 lea rax,[rip+0x249010] # 250468 <_ZN4test3foo4INIT17h68924fbac7fffb27E>
7458: 48 89 c7 mov rdi,rax
745b: e8 90 f9 ff ff call 6df0 <_ZN3std4sync4once4Once9call_once17h7da719856c51e7aaE>
7460: 58 pop rax
7461: c3 ret
7462: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
7469: 00 00 00
746c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
...
となっており、foo::<i64>
とfoo::<isize>
はそれぞれ_ZN4test3foo17h61cadb66d9062e2eE
と_ZN4test3foo17h09b087b10e1d5121E
という、別の名前を持った別のシンボルとして扱われているわけです。
そうすると、foo::<i64>
とfoo::<isize>
は別の関数なのだから、別の関数の中のstatic変数は違う実体を指していてほしいと思います。しかし、上記のアセンブラの0x7471, 0x7451あたりでINIT変数をraxレジスタにロードしているあたりを見ると、同じ場所を指しています。そのため、INITは一回しか呼ばれません!
(ちなみに、C++だと
#include <iostream>
using namespace std;
template <class X> int hoge(X fuga) {
static int AAA = 0;
return AAA++;
}
int main() {
cout << hoge(1) << endl;
cout << hoge(1) << endl;
cout << hoge(1) << endl;
cout << hoge(1.0) << endl;
cout << hoge(1.0) << endl;
}
は、期待したとおり0 1 2 0 1
とoutputを返します。)
なんでそうなっているか
あまり納得がいかなかったので、stackoverflowやrust-lang/rfcsで聞いてみました。
stackoverflow
これです。とりあえず、そういうもんだ、どうにかしたければHashMap
を使って動的になんとかしてね、というのがお答えでした。
rust-lang/rfcs
どうやら言語仕様としてこのようになっていることは上の質問でも理解できたので、なんでそうなっているかを聞きたかったのと、あわよくばなんとかしてもらおうと思ったので、rust-lang/rfcsで聞いてみました。有識者っぽい人の回答によると
FWIW the Rust semantics are "if you can write outside a function, putting it inside a block expression will only change where you can refer to it by name and nothing else".
So you can embed entire modules in an expression - not only in functions but also array lengths, and they behave the same as outside, except name resolution can get a bit confusing in some cases.
だから、だそうです。でも、機械語から「foo::<i64>
とfoo::<isize>
は別の関数」と想像してしまうと、やはり上のような間違いを生んでしまうことになると思うのですが…。
ただ、このように考え、さらにT
というのを普通の引数と同じようなものだと思えば、確かにgenericsの異なるinstance同士でもstatic変数は共有されるべきであるという認識に達するような気もします。関数の実体とか機械語中のシンボルだとか言い出すと、この考え方は極めて非直感的であるような気がしてならないのですが…。
結論
うーん、わからないw
そういえば最近有効になったAssociated constでは、定数が来るべきところには型変数のAssociated constを持ってくることができません。例えば、以下のコードはコンパイルエラーです。
trait HasConst2 {
const C: usize;
}
struct A;
impl HasConst2 for A {
const C: usize = 1usize;
}
fn gen_array<T: HasConst2>(_: T) {
let array = [0i64; T::C];
println!("{:?}", array);
}
実際に機械語に訳す中では、let array = [0i64; T::C];
は単にrspを定数ずらすコードであり、その定数はコンパイル時に決定しているので、原理的には普通の配列宣言とは変わらないはずです。これも、型変数を普通の引数のようなものだと思えば確かにコンパイルエラーになるべきであるような気もします。
随分とりとめのない文章になってしまいましたが、有識者のみなさまのご意見をいただけたらうれしいです。
(なおわたしは型理論とかほとんど知らないので、自明な質問だったらすいません…)