エージェントがレガシーC/C++の前で迷うのは、コードが複雑だからではなく、自分の作業が正しいかどうかを確かめる手段がないからです。モダナイゼーションの最初の投資先は、言語移行ではなく検証環境です。

チョン・ドヒョン - ROBOCO首席コンサルタント


バイブコーディング導入のコンサルティングをしていると、似たような悩みをよく耳にします。「うちのプロジェクトは複雑すぎて、エージェントが指示どおりに仕事をしてくれません」。最初は、ドメインが特殊なのか、プロンプトの書き方に問題があるのだろうと考えていました。しかし事例が積み重なるにつれて、共通点が見えてきました。こう話すチームのプロジェクトは、ほとんどが 20年以上前から続くC/C++のコードベース だったのです。

同じチームでも、新しく作ったPythonツールやWeb管理画面ではエージェントをうまく使いこなしていました。したがって、問題はチームの能力やエージェントそのものよりも、コードベースの側にあると考えるのが自然です。本記事では、こうしたレガシープロジェクトをバイブコーディングでモダナイズする方法を、公開事例と研究をもとに整理します。根拠資料は2026年10月15日時点で調査した公開事例・論文・ツールドキュメントであり、各数値は発表主体による自己報告である点もあわせてお断りしておきます。

本記事は、レガシーコードベースのモダナイゼーションを扱う2部作の1本目です。第1部ではC/C++で書かれたレガシーアプリケーションを扱い、第2部ではPHPで書かれたレガシーWebアプリケーションのモダナイゼーション戦略を扱います。

シリーズ:バイブコーディングでレガシーをモダナイズする

  1. 20年もののC/C++アプリケーション(本記事)
  2. Webの歴史とともに積み重なったPHP

TL;DR

  • エージェントがレガシーC/C++で失敗する主な理由は、ビルドを再現できないこと、ポインタの所有権がコードに表れていないこと、変更を検証するテストがないことです(これは私のコンサルティング経験にもとづく解釈です)。
  • 公開されている成功事例(Google giflib、Wiz fastText、Rukuzu)はいずれも、範囲の狭いモジュールを強力な検証とともに 置き換えています。数百万行を一度に別の言語へ翻訳した事例ではありません。
  • 推奨するのは全面的な書き直しではなく、段階的なモダナイゼーションです。既存のコアはModern C++で安定化させ、境界が狭くメモリ欠陥の多いモジュールだけを選択的にRustなどへ移します。
  • エージェントは調査・変換・テスト・反復修正を担い、人は所有権の設計・互換性の判断・リリースに責任を持ちます。
  • 最初の90日の目標は全面移行ではなく、代表的なモジュールを1つ本番移行し、同じ手順を繰り返せることを証明することです。

1. エージェントはなぜレガシーC/C++で迷うのか

コーディングエージェントは、コードを読み、修正し、ビルドとテストを実行して結果を確認するというループで仕事をします。このループの最後の段階である「確認」が塞がれると、エージェントは自分が修正したコードが正しいかどうかを知ることができません。私が現場で見てきた20年もののC/C++プロジェクトでは、このループが何か所も途切れていました。

第一に、ビルドを再現できません。 特定の開発者のPCに合わせたビルド設定、バージョンが固定されていないベンダーSDK、数十個のプリプロセッサマクロと条件付きコンパイルが絡み合っています。エージェントがクリーンな環境でビルドを実行できなければ、自分の変更がコンパイルできるかどうかすら確認できません。また、同じ関数でも、どのマクロが有効かによって実際にコンパイルされるコードが変わります。実際のコンパイルオプションなしにソースファイルだけを読んだエージェントは、存在しないコードパスを根拠に判断してしまいます。

第二に、ポインタの所有権がコードに表れていません。 古いC/C++コードでは、1つの T* が「このオブジェクトは自分が解放しなければならない」という意味の場合もあれば、「一時的に借りているだけ」という意味の場合もあります。その区別はコードではなく、20年前の作者の頭の中や、すでに失われたWikiの中にあります。エージェントがgrepで呼び出し箇所を見つけても、オブジェクトの破棄後にコールバックが実行されるのか、コンテナの再割り当てによって参照が無効になるのかまではわかりません。そのため、エージェントの修正はもっともらしく見えても、解放後使用(use-after-free)や二重解放を新たに生み出しかねません。

第三に、変更を検証するテストがありません。 テストがあったとしても、少数の正常系だけを扱っていることが多いです。人間の開発者はこの穴を経験と手動テストで埋めてきましたが、エージェントにはそのようなセーフティネットがありません。結果として、エージェントの生成速度は 検証されていない変更量 を増やすだけになります。

まとめると、これらのプロジェクトが「複雑すぎる」からエージェントが失敗するという診断は、半分しか正しくありません。より正確な診断は、エージェントが成否を判断する手段を持っていないということです。だからこそ、モダナイゼーションの最初の投資先は言語移行ではなく、エージェントがオリジナルと新しい実装をビルドし、同じ入力で比較できる検証環境 であるべきです。

2. 公開事例が実際に示していること

エージェントでC/C++をモダナイズした公開事例は、まだ多くありません。そして数字だけを切り出すと、誇張して受け取られやすいものです。以下の表は主要な事例をまとめたうえで、各数値が何を証明し、何を証明していないのかをあわせて記しています。

事例 移行元と移行先 発表された結果 この結果が証明していないこと
Google giflib1 C 約3千行 → Rust 3千万個以上のGIFで回帰比較、2億回以上の差分ファジング、性能は中立 大規模アプリケーションを一括翻訳できること
Wiz fastText2 C++ 約2万行 → Go 2日間で5,434行のGo、保有するモデル検証テストに100%合格 すべてのAPI・プラットフォーム・学習機能の同等性
Loxation Rukuzu3 Kuzu C++ 約20万行 → Rust 報告時点で15個のcrate、2,700件以上のテスト 20万行全体の機能同等性の完了
Google IDの型変更4 32ビット → 64ビットIDへの横断的変更 変更595件のうち74.45%をLLMが生成、総作業時間を約50%削減と推定 言語の全面置き換えの効果
Bun5 Zig → Rust(C++ではない) 2週間未満で100万行以上を生成、既存CIをすべて通過 本番での同等性(通過後も回帰が19件見つかった)

これらの事例には繰り返し現れるパターンがあります。GoogleはSIMDやアセンブリを含まない小さなライブラリを選び、C ABI(バイナリレベルの呼び出し規約)をそのまま維持する置き換え実装を目標にしました。初期の変換後、FFI(言語間の呼び出し境界)のメモリ管理が健全でないという問題が見つかり、専門家がunsafeコードを直接レビューしました。Rukuzuは、既存のC++バックエンドと新しいRustバックエンドが同じ抽象インターフェースを実装するようにし、ビルドオプションでどちらかを選べるようにしました。どちらの事例も、まず置き換え可能な境界を作り、その境界の背後で新旧の実装を比較 しています。

Bunの事例は、逆方向の教訓を与えてくれます。既存のCIテストをすべて通過したにもかかわらず、本番で回帰が19件発生しました。API換算のコストも約16万5千ドルでした。テストの合格と本番での同等性は別物であり、大規模な並列化にはコストもかかることを示しています。

研究の側も同様です。C→Rust自動変換の研究(ORBIT、ACToR、RustAssure)は、小規模なベンチマークで90%前後のテスト合格率を報告しています。6 しかし、これらの数値は平均数百行規模のCプログラムから得られたものです。C++のテンプレート・継承・例外・マクロ・静的初期化・プラグインABIはまた別の難しさを抱えているため、これらの数値を数百万行規模のC++製品の自動移行成功率として読んではいけません。また、C2Rustのような変換器は、Cコードを機能を保ったまま unsafe Rust に移すための出発点にすぎません。Rustファイルが生成されコンパイルできたという事実だけで、メモリ安全性が得られたとは言えません。7

3. 事例から導いた5つの原則

言語移行を成果指標にしない。 同じC++のままでも、所有権が明確になり回帰が減ったのであれば、モダナイゼーションは成功です。逆に、Rustへ移した後にunsafeブロックが広がり、FFIやアップストリームの保守コストが膨らんだのであれば、目標を見直すべきです。コンテナ化やサービス分割はデプロイや障害の影響範囲には役立ちますが、ポインタの欠陥そのものを直すわけではありません。

変換と再設計を分ける。 最初の段階では、構造と外部の振る舞いを最大限維持しながら、検証可能な移行を行います。所有権・データ構造・並行性モデルの改善は、その次の段階で行います。両方を同時に行うと、出力が変わったときに、それが翻訳ミスなのか設計変更の結果なのかを区別できなくなります。AnthropicのCode Migration Kitも、構造を保つ移行をデフォルトとしています。8

決定的なツールとエージェントの判断を分ける。 AST(構文木)で正確に特定できるAPI置換や構文修正は、clang-tidyやClang LibToolingのような決定的なツールに任せます。エージェントは、変換ルールの草案作成、例外的なケースの処理、失敗原因の分析、テストの補強を担当します。GoogleのID変更の事例も、静的解析とコード検索で変更箇所を見つけ、LLMが文脈に合わせて修正し、自動検証を経て人が受け入れる構造でした。4

テストと合格基準を実装エージェントから独立させる。 実装を担当するエージェントが期待値・テスト・合格基準を変更できると、エージェントはテストを緩めて通すという安易な道を選びかねません。新しい入力を生成する検証役を分離し、実装の過程で使わないholdoutデータを別に用意します。

リポジトリ全体ではなく、必要な根拠を供給する。 数百万行をまるごとプロンプトに詰め込むのではなく、実際のビルド設定、シンボルの定義と参照、モジュール契約、失敗ログ、関連テストを供給します。作業中に見つけたルールは、チャットの記憶ではなくファイルに残します。この原則は次の節でより具体的に扱います。

4. エージェントが読めるコードベースをつくる

エージェントにレガシーをうまく扱わせるには、まずエージェントが人間の開発者と同じレベルの情報を見られるようにする必要があります。出発点は、実際のコンパイルオプションの復元です。CMakeプロジェクトであれば compile_commands.json を生成し、他のビルドシステムであればBearのようなツールでコンパイルコマンドを収集します。このファイルがあってはじめて、clangdのような言語サーバーが、マクロや条件付きコンパイルを反映した定義・参照・型情報を正確に返せます。ただし、MSVC固有の拡張やクロスコンパイル環境では、Clangベースの解析が実際のビルドと異なる場合があるため、解析結果がどのビルドバリアントに該当するのかを明記しておく必要があります。9

次に、エージェントのためのドキュメントをリポジトリ内に置きます。これらのドキュメントは、人間の新任開発者にとってもそのまま役立ちます。

ドキュメント 内容 更新タイミング
AGENTS.md などの運用ルール ビルド・テストコマンド、禁止された変更、完了条件 ツールチェーンや検証ポリシーが変わったとき
モジュール契約 入出力、エラー、所有権、スレッド、ABI インターフェースが変わったとき
シンボル対応表 オリジナルと新しいシンボルの対応、進捗状況、欠落 作業が1つ終わるたび
ADR(設計判断記録) 言語・境界・例外・性能に関する決定とその理由 設計を決定したとき
欠陥コーパス 最小再現入力と回帰テスト 新しい欠陥を発見したとき

Rukuzuの事例は、この方法をよく示しています。そのチームはセッションごとに進捗を要約し、オリジナルと新しいシンボルを照合し、差分と所有権の決定を文書化したうえで、依存関係の順に実装しました。各段階でbuild・test・clippyを実行し、作業中に見つけた落とし穴はコマンドドキュメントに蓄積し続けました。3 こうしておけば、エージェントのコンテキストが初期化されても、次のセッションで同じミスを繰り返さずに済みます。

5. 核心となる難題:ポインタとオブジェクトの寿命

レガシーC/C++のモダナイゼーションで最も難しいのは、ポインタの所有権を明らかにすることです。エージェントに「raw pointerをスマートポインタに置き換えよ」と一括で指示するのは危険です。ポインタごとに、生成箇所、所有者、解放関数、エイリアス(同じオブジェクトを指す別のポインタ)、格納場所、アクセスするスレッド、非同期の終了条件をまず確認しなければなりません。以下の表は、既存のパターンごとの基本的な移行方向です。

既存のパターン 基本の移行先 注意点
単独所有の T* 値型または unique_ptr 既存の解放経路とエイリアスを先に把握する
実際の共同所有 shared_ptr、必要に応じて weak_ptr 循環参照とデータ競合は別の問題
一時的な非所有参照 参照または非所有ポインタ 元のオブジェクトより長く保持できないよう契約で定める
バッファと長さ vector、array、span span は非所有であり、寿命を証明しない
C SDKのハンドル 専用のRAIIラッパー 必ず対になる解放関数とアロケータを使う
非同期での this キャプチャ キャンセル、join、弱参照 購読解除と終了順序を設計する

RAIIは、リソースをオブジェクトの寿命に結びつけて自動的に解放するC++の手法です。メモリだけでなく、ファイル・ソケット・ロック・スレッド・イベント購読にも適用できます。ただし、QtのQObjectの親子所有権、COMの参照カウント、Unrealのリフレクションのように、フレームワークごとに固有の寿命ルールがあります。こうしたコードを標準のスマートポインタで一律に置き換えてはいけません。

そこで私は、役割を次のように分けています。エージェントは呼び出し・格納・解放・コールバックの経路を追跡し、根拠付きの所有権表を作成します。人はその表をレビューし、最終的な寿命の設計を承認します。確認できなかった寿命は「未確定」として残し、根拠がなければエージェントが変更しないよう指示文に明記します。また、malloc/free と new/delete の組み合わせを入れ替える変更や、Rustで割り当てたメモリをCの free で解放する変更は、明示的に禁止します。

完了基準にも注意が必要です。成功の基準はraw pointerの数の減少ではなく、所有権の明確さ、欠陥を再現するテスト、新たな解析警告がないことです。非所有の境界にraw pointerが残ることは許容できます。

6. どの言語へ移行するか

すべてのモジュールを同じ言語へ移す必要はありません。以下の表は、モジュールの性格ごとの基本的な選択と、別の選択が必要になる条件です。

判断対象 基本の選択 別の選択が必要になる条件
既存のコア Modern C++(C++17/20)とRAIIで安定化 メモリ欠陥が続き、境界を分離できるならRustを検討
入力パーサーとプロトコル 狭いインターフェースの背後でRustのパイロット ABIの制約が大きい、または検証資料が不足しているなら、まずC/C++を改善
業務ロジックと管理機能 デプロイ環境に合ったマネージド言語(C#、Java、Goなど) リアルタイム性・メモリ・呼び出しコストの制約が厳しければネイティブを維持
修正が難しいエンジン アダプタまたは別プロセスで隔離 IPCの遅延やデータコピーが許容できなければプロセス内の境界

Rustを選ぶ場合でも、CとC++では道筋が異なります。C中心のモジュールは、C2Rustで機能を保ったunsafe Rustを生成し、その後段階的に安全なコードへ書き換えていくことができます。C++中心のモジュールは、狭いC ABIやCXXのような連携レイヤーで境界を作り、インターフェース契約に沿って慣用的なRustで再実装するほうが現実的です。このとき、borrow checkerを通すために Rc・Arc・Mutex を機械的に増やすことは避けるべきです。それは元の所有権の問題を別の形で隠すだけです。

言語を変えるときは、数値処理の違いも確認する必要があります。整数のオーバーフロー、浮動小数点とNaN、エンディアン、エンコーディング、ロケールの挙動は言語ごとに異なります。C/C++でたまたま成り立っていた挙動や未定義動作(UB)は、移行先の言語では変わる可能性があります。

7. 段階ごとの手順と通過条件

モダナイゼーションは以下の段階で進め、各段階では次へ進むための証拠を求めます。各段階の名称は本記事で定義したものです。

段階 人とエージェントの作業 通過条件
G0 再現 ツールチェーン・依存関係の固定、CIビルド クリーンな環境での基準バージョンのビルド・テストログ
G1 理解 業務フロー・シンボル・所有権の分析 担当者が確認した契約と不確実性のリスト
G2 保護 特性テスト、障害の再現、比較ハーネス オリジナルを同じ入力で繰り返し実行できる
G3 設計 境界・言語・エラー・スレッドのポリシー決定 ADR、インターフェース、ロールバック計画、性能予算
G4 変換 限定されたファイル範囲での小さな変更 ビルド・静的解析・回帰テストに合格
G5 反証 差分実行、ファジング、並行性・性能の検証 原因が分類されていない出力差分が0件
G6 移行 shadow実行、カナリア、フィーチャーフラグ 承認された観測指標と復旧手順を満たす
G7 定着 旧バージョンの削除、ドキュメント、アップストリーム管理 新しい実装のオーナーと運用手順が確定

エージェントが実質的にスピードを上げるのは、G1(調査)、G2(テスト作成)、G4(変換)、G5(反証用の入力生成)の区間です。一方でG0は、人が主導しなければならない場合が多くあります。社内SDKのライセンス、ビルドサーバーへのアクセス、ベンダーのツールチェーンの確保は、エージェントには解決できないからです。私の経験では、「エージェントが仕事をしてくれない」と話すチームの多くはG0で止まっています。

G5の差分実行は、同じ入力と初期状態でオリジナルと新しい実装を実行し、結果を正規化して比較する方法です。比較対象は戻り値だけでなく、ファイル・DBの変更、エラーコード、出力の順序、終了状態、メモリ使用量まで含みます。オリジナルにUBが含まれている場合、オリジナルの実行結果を正解とみなすことはできません。そのため差分が出たら、レガシーの欠陥、意図した互換動作、新たな欠陥、環境の違いのどれに当たるのかを分類し、業務担当者と技術担当者が判断します。Google giflibのチームも、この過程で既存のCコードの範囲外書き込みを発見しました。1 オリジナルのメモリエラーまで保存する必要はなく、発見した欠陥は意図的な動作変更として記録します。

修正の繰り返しには上限を設けます。私はデフォルト値として、1つの作業につき修正3回、または事前に決めた時間・コストの上限を提案しています。失敗の繰り返し、新たなunsafeブロック、ABIの変更、原因不明の出力差分、テストの削除が現れたら、作業を止めて診断の段階に戻ります。完了報告には、基準コミット、変更したシンボル、実行したコマンドと終了コード、テスト結果を含めさせます。実行していない検査は「未実行」として残し、エージェントの自然言語による主張だけで合格扱いにはしません。

8. 最初の90日の計画と、立ち止まるべきとき

以下は、大規模プロジェクトで最初のモジュールを移行するための90日計画の例です。実際のスケジュールは、ビルド復旧の難易度、テストの穴、サポート対象プラットフォームの数によって変わります。

期間 作業 成果物
1〜2週目 ビルド・依存関係・障害履歴・テストの診断 ベースラインと候補モジュール2〜3個
3〜4週目 契約、再現コーパス、差分ハーネスの構築 パイロット1つの選定、言語・境界のADR
5〜8週目 モジュールの改善・移植と反復検証 比較可能な新しい実装と欠陥台帳
9〜10週目 holdout・ファジング・性能・運用試験 G5の証拠とロールバックのリハーサル
11〜12週目 shadow実行、カナリア、移行、振り返り 本番での結果と次のモジュールへの拡大判断

最初のパイロット候補には、パーサー、変換器、計算ライブラリ、独立したCLIのように、入力と出力が明確なモジュールが適しています。UIフレームワークと入り混じったコア、複数のスレッドが共有するグローバルなオブジェクトグラフ、ドライバーや認証モジュールは2番目以降に回します。業務価値(障害や修正の頻度)、境界の明確さ、検証可能性が高いモジュールを選び、結合度と移行コストが高いモジュールは後回しにします。スコアだけで決めず、候補モジュールのビルドと代表的な入力を実際に実行して、前提を確かめます。

エージェントの構成もシンプルに始めます。最初は主力のエージェント1つが役割を順番にこなし、検証の段階だけに別のセッションや担当者を置きます。並列エージェントの数は、開発者の数ではなく、ビルドリソースとレビューの処理能力に合わせます。公開事例にある数百エージェントの構成を、最初の構成としてそのまま真似してはいけません。

次のような状態が現れたら、エージェントの数を増やすのではなく、再現・契約・分割の段階に戻るべきです。

  • ビルドが特定の個人の環境に依存している、またはオリジナルを実行できない。
  • 期待される動作を決められる人がおらず、テストを減らさなければ合格しない。
  • unsafeブロックや共有された可変状態が増え続けている。
  • 共通の型の変更のせいで、すべての作業が互いに衝突する。

対象プラットフォーム・認証・ベンダーサポートが移行先の言語を許容しない、あるいは検証環境そのものを構築できないモジュールもあります。こうしたモジュールは、言語移行の代わりに、既存のC/C++の改善、プロセスの隔離、保守されている代替製品の採用によって目標を達成するほうがよいでしょう。

おわりに

「うちのコードは複雑すぎてエージェントには無理だ」という言葉は、実は「うちのコードは複雑すぎて、変更が正しいかどうかを確かめる方法がない」という言葉に近いものです。この問題はエージェントが登場する前から存在していました。人間の開発者は、経験と慎重さでこの穴を埋めてきただけです。エージェントにはその穴を埋める経験がないため、問題をより早く、よりはっきりと浮かび上がらせます。

だからこそ、レガシーC/C++のモダナイゼーションは、エージェントに翻訳を任せることではなく、エージェントが働ける検証環境をつくることから始めるべきです。再現可能なビルド、コンパイルコンテキスト、モジュール契約、差分比較ハーネスが揃えば、エージェントは調査・変換・反証を人間よりはるかに速く繰り返します。この構造は、本ブログで取り上げたソフトウェアファクトリーと同じ方向を向いています。10 特に証拠でふるいにかけるファクトリーで扱ったように、変更を受け入れる基準はエージェントの主張ではなく、実行の証拠であるべきです。11

本記事のスケジュール・しきい値・構成は、公開事例にもとづく提案です。実際に適用する前に、対象のリポジトリを診断して調整してください。

次回の記事では、レガシーWeb、なかでもPHPで書かれたWebアプリケーションをバイブコーディングでモダナイズする戦略を取り上げます。


  1. Google Bug Hunters, “Scaling Memory Safety: AI Assisted Rewrites of C/C++ Dependencies to Rust” (2026-08-24): https://bughunters.google.com/blog/scaling-memory-safety ↩︎ ↩︎

  2. Anthropic、Wizの顧客事例(顧客による自己報告): https://claude.com/customers/wiz ↩︎

  3. Loxation, “Rukuzu: Porting a 200,000 line codebase from C++ to Rust” (2026-03-04): https://www.loxation.com/blog/posts/article_porting_workflow/ ↩︎ ↩︎

  4. Ziftci et al., “Migrating Code At Scale With LLMs At Google” (FSE 2025): https://arxiv.org/html/2504.09691 、Google Researchブログ: https://research.google/blog/accelerating-code-migrations-with-ai/ ↩︎ ↩︎

  5. Anthropic, “How Anthropic runs large scale code migrations with Claude Code” (2026-07-16): https://claude.com/blog/ai-code-migration ↩︎

  6. ORBIT: https://arxiv.org/html/2604.12048 、ACToR: https://arxiv.org/html/2510.03879 、RustAssure: https://arxiv.org/html/2510.07604 。3つの研究は異なる単位とデータセットを用いているため、数値を直接比較することはできません。 ↩︎

  7. C2Rust Manual: https://c2rust.com/manual/ ↩︎

  8. Anthropic、Code Migration Kit(reference codeであり、積極的には保守されていないと明記): https://github.com/anthropics/code-migration-kit-with-claude-code ↩︎

  9. clangdのインストールとコンパイルコンテキスト: https://clangd.llvm.org/installation ↩︎

  10. バイブコーディングを超えて、ソフトウェアファクトリーへ: https://roboco.io/ja/posts/beyond-vibe-coding-software-factory/ ↩︎

  11. オープンソースプロジェクトから学ぶソフトウェアファクトリーのベストプラクティス(3/4):証拠でふるいにかけるファクトリー: https://roboco.io/ja/posts/software-factory-evidence-before-fix/ ↩︎