RustエラーE0015の修正:定数内で関数を呼び出せない理由

intermediate🦀 Rust2026-06-28| Rust (Stable 1.80+), Cargo, 任意のOS

Error Message

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
#rust#コンパイルエラー#const#lazylock

問題点PythonやJavaScriptのような言語に慣れていると、単にコンストラクタを呼び出すだけでグローバルな設定用マップを初期化できると期待するかもしれません。しかし、Rustはコンパイルプロセスに対して非常に慎重です。Rustでは、コードのビルド時に起こることと、実行時に起こることを厳格に区別します。

以下は、よくこの問題を引き起こすコードスニペットです:

use std::collections::HashMap;

// これは E0015 を発生させます
const MY_CONFIG: HashMap = HashMap::new();

fn main() {
    println!("{:?}", MY_CONFIG);
}

cargo buildを実行すると、コンパイラは次のメッセージを表示して停止します:

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
 --> src/main.rs:4:39
  |
4 | const MY_CONFIG: HashMap = HashMap::new();
  |                                       ^^^^^^^^^^^^^^
  |
  = note: calls in constants are limited to constant functions, tuple structs and tuple variants

なぜこれが起こるのかconstキーワードは、コンパイル時に一度だけ値を計算し、それが使用されるすべての場所にインライン展開するようRustに指示します。これを安全に行うために、コンパイラはconstブロック内で呼び出されるすべての関数がconst fnとしてマークされていることを要求します。これらは、プログラム全体を実行することなくコンパイラが実行できる決定論的な関数です。

なぜHashMap::new()const fnではないのか不思議に思うかもしれません。その理由は、HashMapがDoS攻撃を防ぐためのハッシュアルゴリズムにランダムシードを使用しているからです。そのランダム性を生成するにはオペレーティングシステムとのやり取りが必要ですが、コンパイラはバイナリのビルド中にそれを行うことができません。

解決策1:LazyLockを使用する(モダンな標準)Rust 1.80以降を使用している場合、std::sync::LazyLockが最適です。これを使用すると、最初にアクセスした瞬間まで初期化を待機するグローバル変数を定義できます。これにより、スレッド安全性を維持しながらコンパイル時の制限を回避できます。

use std::collections::HashMap;
use std::sync::LazyLock;

// LazyLockはMY_CONFIGが最初に使用されたときにのみ初期化される
static MY_CONFIG: LazyLock> = LazyLock::new(|| {
    let mut m = HashMap::new();
    m.insert("api_version", 2);
    m.insert("timeout_ms", 5000);
    m
});

fn main() {
    // HashMapは実際にはここで作成される
    println!("Timeout: {:?}", MY_CONFIG.get("timeout_ms"));
}

constからstaticに切り替えたことに注意してください。これにより、データが至る所にコピーされるのではなく、単一の固定されたメモリ位置に保持されるようになります。

解決策2:動的なデータにOnceLockを使用するプログラムが実行されるまでグローバル変数の値がわからない場合があります。例えば、環境変数からDATABASE_URLを読み込むような場合です。このようなケースでは、std::sync::OnceLockがより良い選択肢となります。

use std::collections::HashMap;
use std::sync::OnceLock;

static APP_CACHE: OnceLock> = OnceLock::new();

fn main() {
    // 実行時に一度だけキャッシュを初期化する
    let cache = APP_CACHE.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert("session_id".to_string(), "abc-123".to_string());
        m
    });

    println!("Current session: {}", cache["session_id"]);
}

解決策3:独自の関数を'const'に変換する独自の関数を呼び出しているときにE0015が発生した場合は、const修飾子を追加することで解決できることがよくあります。これは、関数がヒープ割り当てや複雑なI/Oを実行しない限り機能します。

// 'const'を追加することで、コンパイル中に実行できるようになる
const fn get_max_threads(cores: u32) -> u32 {
    cores * 2
}

const WORKER_LIMIT: u32 = get_max_threads(8);

fn main() {
    println!("Max workers: {}", WORKER_LIMIT);
}

解決策4:オーバーヘッドゼロの静的配列を使用する小規模で固定されたルックアップの場合、HashMapは過剰かもしれません。タプルの静的配列の方が高速で、コンパイラにとっても扱いやすいことがよくあります。このアプローチでは、実行時の初期化時間はゼロです。

// 定数には単純なペアの配列で十分なことが多い
const SERVER_SETTINGS: [(&str, u16); 3] = [
    ("web", 8080),
    ("api", 9000),
    ("metrics", 9100),
];

fn main() {
    for (service, port) in SERVER_SETTINGS {
        println!("Service {} is on port {}", service, port);
    }
}

まとめと予防策- constでヒープ型を避ける: StringVecHashMapconst内で直接使用しないでください。- LazyLockを活用する: これは、モダンなRustでグローバルな状態を扱うための最も洗練された方法です。- ドキュメントを確認する: 標準ライブラリで関数が明示的にconst fnとしてマークされていない場合、それをconst宣言で使用することはできません。- パフォーマンスのヒント: コンパイル時に高性能で不変なマップが必要な場合は、phf(Perfect Hash Function)クレートを検討してください。

Related Error Notes