
Claude Code に C/C++/Rust/Zig で同じ課題を実装させたら、言語ごとのふるまいに差は出るのか
はじめに
AI にコードを書かせるとき、プログラミング言語の選択は生成品質に影響するのでしょうか。Rust のボローチェッカは AI の修理を助けるのか、C の単純さは AI に有利に働くのか……。Claude Code (Opus 4.6) に同一の課題を C, C++, Rust, Zig の 4 言語で実装させ、言語ごとの差異を観察してみました。
Rust とは
Rust は Mozilla で開発が始まったシステムプログラミング言語です。現在は Rust Foundation のもとコミュニティ主導で開発されています。所有権システムとボローチェッカにより、メモリ安全性とスレッド安全性をコンパイル時に保証します。
Zig とは
Zig は C の代替を目指すシステムプログラミング言語です。手動メモリ管理を採用しつつ、コンパイル時の計算 (comptime) やランタイム安全性チェックで安全性を補強しています。
対象読者
- AI コード生成に関心のあるソフトウェアエンジニア
- プログラミング言語の安全性機構に関心のある開発者
- Claude Code の実用性を知りたい方
検証環境
- OS: Windows 11 Home (Build 26200)
- CPU: Intel Core i7-11700F, RAM: 32 GB
- C/C++: MSVC v143 (Visual Studio 2022), CMake 4.2.1, C は Win32 API (CreateThread) でスレッド実装
- Rust: rustc 1.93.1
- Zig: 0.15.2
- AI: Claude Code (Opus 4.6)
参考
検証の計画
言語の安全性機構が AI に影響を与えるとすれば、メモリ管理と並行処理が最も差が出やすい領域でしょう。C/C++ では手動管理が求められ、Rust ではコンパイラが厳格に検査し、Zig はランタイムチェックで補強するという、言語ごとのアプローチが最も分かれる領域だからです。
仮説としては、以下のいずれも起こり得ると考えました。
- Rust のボローチェッカが修理を助け、修正ラウンドが最少になる
- 逆にボローチェッカが AI を苦しめ、コンパイルエラーとの対話にラウンドを消費する
- C の単純さが AI に有利に働き、手動メモリ管理でも一発合格する
- 言語による差はほとんどなく、生成品質は言語に依存しない
検証方法
今回の検証では 2 つの課題を用意しました。
- 課題 A: LRU キャッシュ
メモリ管理が焦点で、所有権・寿命・解放責務が問われる。 - 課題 B: Thread Pool
並行処理が焦点で、shutdown・join・条件変数の同期が問われる。
各課題に対し、L1 テスト (仕様テスト 12 項目) と L2 ストレステスト (seed 固定、反復実行) を定義しました。AI が生成したコードがこれらのテストに合格するまで最大 5 ラウンドの修理を許可します。
交絡変数の制御
言語間で学習効果が持ち越されないよう、各言語の実装はそれぞれ独立した Claude Code セッションで行いました。各セッションに提供する情報は仕様書、テストコード、ビルドコマンドの 3 点のみです。

また、完成コードに対する理解度テストも実施しました。AI が生成したコードの可読性が言語によって異なるのであれば、後から別の AI がそのコードを保守する際にも差が出るはずです。これを確かめるため、別のセッションに仕様書を渡さずコードだけを読ませ、概要説明、問題点の指摘、機能追加の 3 タスクを課しました。
結果 1: メモリ管理課題では差が出なかった
LRU キャッシュの結果です。
| 指標 | C | C++ | Rust | Zig |
|---|---|---|---|---|
| First-pass result | 全通過 | 全通過 | 全通過 | 全通過 |
| Rounds to green | 0 | 0 | 0 | 0 |
| Compile-fix cycles | 0 | 0 | 0 | 0 |
| Stress survival | 全通過 | 全通過 | 全通過 | 全通過 |
4 言語とも初回で L1 テスト 12/12 に合格し、ストレステスト (10,000 反復) も通過しました。修理ラウンドは 0 回です。アルゴリズムの選択も 4 言語で共通しており、いずれもハッシュマップ + 双方向リンクリストという LRU キャッシュの定番パターンを採用しました。ただし Rust だけは、ボローチェッカとの折り合いのために独自の適応を見せています。
Rust のインデックスベースリスト
C の実装ではポインタでノードを連結します。
typedef struct Entry {
int32_t key;
uint8_t *value;
size_t value_len;
struct Entry *prev;
struct Entry *next;
struct Entry *hash_next;
} Entry;
一方 Rust の実装では、ポインタの代わりに Vec のインデックスでノードを連結します。
struct Node {
key: i32,
value: Vec<u8>,
prev: usize, // ポインタではなくインデックス
next: usize,
}
pub struct LRUCache {
capacity: usize,
map: HashMap<i32, usize>,
nodes: Vec<Node>, // ノードの arena
head: usize, // センチネル (index 0)
tail: usize, // センチネル (index 1)
}
Rust で双方向リンクリストをポインタベースで実装すると、ノードが前後のノード両方から可変借用される形になり、unsafe なしでは成立しません。AI はこれを回避するために Vec<Node> を arena として使い、インデックスを疑似ポインタとして機能させる戦略を取りました。unsafe ブロックは 0 です。これは Rust コミュニティで知られたパターン です。AI が自然にこの戦略を選択した点は注目に値します。
結果 2: 並行処理課題で差が現れた
Thread Pool の結果です。指標の定義は次のとおりです。
- Rounds to green: テスト全通過までに要した修正ラウンド数 (生成→ビルド→テスト→失敗提示→修正 を 1 ラウンドとする)
- Compile-fix cycles: テスト実行前にコンパイルエラーで止まり修正した回数
- Regression count: 修正により別のテストが新たに失敗した回数
| 指標 | C | C++ | Rust | Zig |
|---|---|---|---|---|
| First-pass result | T08 失敗 | ビルドエラー | T08 失敗 | ビルドエラー |
| Rounds to green | 1 | 2 | 2 | 3 |
| Compile-fix cycles | 0 | 1 | 0 | 1 |
| Regression count | 0 | 0 | 1 | 0 |
| Stress survival | 全通過 | 全通過 | 全通過 | 全通過 |
課題 A とはうってかわって、全言語で修理が発生しました。修理ラウンド数は C: 1、C++: 2、Rust: 2、Zig: 3 です。
全言語共通の壁: テスト側のタイミングレース
4 言語すべてで T08 テスト (submit がブロック中に shutdown が呼ばれるケース) が失敗しました。原因は実装ロジックの誤りではなく、テストコード内のタイミング依存性です。AI は並行処理の実装ロジック自体は概ね正しく生成できましたが、テスト内で submit の完了を待ってから shutdown を呼ぶという制約を初回で正しく扱えませんでした。
言語ごとに異なる失敗の種類
T08 テスト側レースは全言語共通でしたが、それ以外の失敗は言語ごとに異なります。C はテスト側レースの修正のみで 1 ラウンド完了でしたが、他の 3 言語は追加の問題を抱えていました。
- C++
worker_loopを自由関数として定義したが、private なImpl構造体にアクセスできずコンパイルエラー (C2248) - Zig
Zig の API 変更によりstd.time.sleepがstd.Thread.sleepに移動しておりコンパイルエラー。さらにThreadPoolを値返ししたことでワーカースレッドがダングリングポインタを参照しランタイムパニック - Rust
T08 の修正が T09 をリグレッションさせた (修理中に別のテストを壊した唯一の言語)
Zig のランタイム検出が AI を助けた事例
Zig の R2 は興味深い事例です。ThreadPool を値で返すと、構造体がスタック上で移動し、ワーカースレッドが保持するポインタが無効になります。Zig のランタイム境界チェックがこれを index out of bounds として即座に検出し、AI は SharedState をヒープに配置する修正を 1 ラウンドで完了しました。
pub const ThreadPool = struct {
state: *SharedState, // ヒープへのポインタ (値埋め込みではない)
workers: []std.Thread,
pub fn init(thread_count: usize, queue_capacity: usize) ThreadPool {
const alloc = std.heap.page_allocator;
// SharedState をヒープに配置し、安定したアドレスを確保
const state = alloc.create(SharedState) catch @panic("alloc failed");
// ...
}
};
Rust であればこの種の問題はコンパイル時に検出されますが、C であれば未定義動作として静かに進行する可能性があります。 Zig のランタイム検出は、コンパイル時保証がない言語でもエラーの早期発見に貢献した好例です。
結果 3: 理解度テストではどの言語のコードも正確に読めた
完成コードに対し、仕様書なしで別セッションの AI に 3 つのタスクを課しました。
- タスク 1: 概要説明
実装の構造、同期方式、ライフサイクルなどの理解度を 7 項目で評価 - タスク 2: 問題点指摘
指定関数の潜在的リスクを分析 - タスク 3: 機能追加
新しい関数を追加実装させ、既存テストを壊さないか確認
結果は両課題とも全 4 言語でタスク 1: 7/7、タスク 2: 正確、タスク 3: 修理 0 ラウンドでした。実装時には課題 B で言語間の差が現れたにもかかわらず、読解と改修では差が出ませんでした。
ただしタスク 2 の指摘内容には質的な差がありました。課題 A の cache_get の参照無効化について、C/C++/Zig は use-after-free リスクとして指摘したのに対し、Rust は参照無効化がボローチェッカにより防がれることを認識したうえで、&mut self による API の使い勝手への制約として指摘しました。AI は言語の安全性モデルを理解したうえで、同じ問題を言語固有の文脈で再解釈しています。
検証から分かったこと
仮説 1, 2: ボローチェッカの影響
ボローチェッカが修理を助ける、ボローチェッカが AI を苦しめるという 2 つの仮説はどちらも当てはまりませんでした。Rust のコンパイルエラーは 0 回で、ボローチェッカに苦しめられた形跡はありません。課題 A ではインデックスベースリストを自然に選択するなど、最初から折り合いを付けていました。一方で修理ラウンドが最少でもなく (最少は C の 1 回)、唯一リグレッションが発生した言語でもありました。
仮説 3: C の単純さが有利にはたらくか
一部当てはまりました。課題 B で C は最少の 1 ラウンドで合格し、コンパイルエラーもランタイムパニックもなく、テスト側レースの修正のみで済んでいます。ただし課題 A では 4 言語とも差がなかったため、優位性は限定的です。
仮説 4: 生成品質に言語差なし
実態に最も近い仮説でした。 課題 A では完全に当てはまり、課題 B でも全言語共通のテスト側レースが支配的で、言語固有の差は相対的に小さいものでした。
仮説外の発見
C++ と Zig はコンパイル時にエラーを検出し、Zig はランタイムでもダングリングポインタを捕捉しました。Rust は unsafe なしでスレッド安全性を保証しました。しかし、テストのタイミングレースはいずれの安全機構でも防げませんでした。言語安全機構の効果は、エラーメッセージの速さと具体性に表れます。
まとめ
AI にとって言語の選択は生成品質にわずかな影響を与えますが、その影響は課題の複雑さに対して相対的に小さいものでした。 メモリ管理のような既知のパターンでは差が出ず、並行処理で初めて差が顕在化します。言語安全機構はエラーの早期検出に寄与しますが、テスト設計のタイミング制約は全 4 言語で修理を要しており、言語の選択では回避できない課題でした。









