RustのGenericsを触っていて腑に落ちなかったところ

はじめに

最近、emscriptenのval.hのRustラッパーを書いていたのですが、Rustのgenerics周りを触っていて少し引っかかったところがあり、Qiitaに集うRustaceanの皆さんにも聞いてほしくて筆を取りました。

サンプルコード

test.rs
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を定数ずらすコードであり、その定数はコンパイル時に決定しているので、原理的には普通の配列宣言とは変わらないはずです。これも、型変数を普通の引数のようなものだと思えば確かにコンパイルエラーになるべきであるような気もします。

随分とりとめのない文章になってしまいましたが、有識者のみなさまのご意見をいただけたらうれしいです。
(なおわたしは型理論とかほとんど知らないので、自明な質問だったらすいません…)