モダン第9章:【C++17】クロスプラットフォームファイル操作:std::filesystem
Windowsの「\」とUnixの「/」問題の終焉!OSネイティブAPI依存からの完全脱却
ゲーム開発において、セーブデータの保存やアセット(テクスチャ・BGM・ステージ定義)の探索は必須の処理です。しかし、C++03/11の標準ライブラリには「ディレクトリ内のファイル一覧を取得する」機能すら存在せず、Windowsなら Win32 API(FindFirstFile / FindNextFile)、Mac/Linuxなら POSIX(opendir / readdir)と、OSごとの生APIを #ifdef で泥臭く分岐して書くしかありませんでした。パス区切り文字の違い(Windowsの「\」とUnixの「/」)によるバグも絶えませんでした。C++17で標準化された【std::filesystem】は、この長年の悪夢に完全な終止符を打ちました。本章では、スマートなパス結合演算子(/)、ディレクトリ走査イテレータによるアセット自動検出、そしてゲーム業界の例外禁止環境に必須の非例外版エラーハンドリング(std::error_code)を用いた安全なセーブデータ保護手法を体得します。
M9.1 OS依存ファイル操作の泥沼:Win32 API vs POSIX
OSごとにバラバラだったファイル操作APIの苦痛と、std::filesystem::path による完全な統一を比較します。
🐧 ペンギン先輩
シロクマ先生!Windowsで動いていたゲームをMac(またはSteam Deck/Linux)に移植しようとしたら、パスの「\\」と「/」の違いで画像が1枚も読み込めなくなりました!しかもフォルダ一覧取得コードがコンパイルエラーだらけです!
🐻❄️ シロクマ先生
ふぉっふぉっふぉ!それこそが全ゲーム開発者が通る洗礼じゃな!C++14以前は、ファイルやフォルダを扱う標準関数が一切なく、WindowsとUnix系でコードを完全に2重化する必要があったのじゃ。
🐧 ペンギン先輩
1つのコードでWindowsでもSwitchでもMacでも動く、標準のパス操作方法はないんですか?
🐻❄️ シロクマ先生
それこそが C++17 で導入された【std::filesystem】じゃ!`std::filesystem::path` を使えば、パス区切り文字の正規化から親ディレクトリ取得・拡張子変更まで、OSネイティブAPIを1行も書かずに100%クロスプラットフォームで完結するぞ!
M9.2 ディレクトリ走査とアセット自動検出:directory_iterator
フォルダ内の全ファイルを範囲for文でスマートに走査し、再帰探索を行う手法を学びます。
🐧 ペンギン先輩
ゲーム起動時に「stage01/sounds」フォルダの中にある音源ファイルを全部自動で読み込みたいんですが、どう書けばいいですか?
🐻❄️ シロクマ先生
【std::filesystem::directory_iterator】の出番じゃ!なんと範囲ベース for ループで `for (const auto& entry : fs::directory_iterator(dir))` と回すだけで、フォルダ内の全ファイルが手に入るのじゃ!
🐧 ペンギン先輩
えっ!サブフォルダの中身までまとめて掘り下げたいときはどうするんですか?
🐻❄️ シロクマ先生
その時は【fs::recursive_directory_iterator】に差し替えるだけじゃ!深さ何十階層のフォルダツリーでも、1行のfor文ですべて再帰探索してくれるぞ!
M9.3 セーブデータ安全書き出しとエラーハンドリング(例外 vs std::error_code)
ゲーム業界の「例外禁止」規約に準拠した非例外版ファイルAPIと、アトミック保存によるクラッシュ破損防止。
🐧 ペンギン先輩
シロクマ先生!「ディスク容量がいっぱい」や「アクセス権限がない」ときに std::filesystem::copy_file を呼んだら、filesystem_error 例外が飛んできてゲームがクラッシュしてしまいました!うちのチーム、例外(try-catch)使用禁止規約なんですが……!
🐻❄️ シロクマ先生
そこが超重要じゃ!std::filesystem のほぼすべての関数には【第2引数に std::error_code を渡すオーバーロード】が用意されておる!
🐧 ペンギン先輩
std::error_code を渡せば例外が飛ばなくなるんですか!?
🐻❄️ シロクマ先生
その通り!エラーが発生しても例外を投げず、エラー内容を引数の ec に記録して安全に戻ってきてくれる。例外禁止のゲームエンジンでも100%安心して利用できるのじゃ!
💡 このセクションの重要ポイント
- 非例外版 API:std::error_code の活用: `fs::create_directories(dir, ec);` のように std::error_code を渡すことで、C++例外機構を一切使わずにOSエラーを安全に判定できます。
- 2段階アトミック保存パターン: セーブデータを直接上書きせず、まず `save.tmp` に全データを書き込み、成功後に `fs::rename("save.tmp", "save.dat", ec)` で瞬時にアトミック昇格させることで、停電やクラッシュ時のセーブデータ破損を完全防止します。
M9.4 実戦:自作ゲームエンジンのアセットローダー&ホットリロード監視
ファイルの最終更新時刻(last_write_time)を取得し、ゲーム実行中のアセット再読み込みを可能にします。
🐧 ペンギン先輩
シロクマ先生!デザイナーさんがテクスチャ画像やステージテキストを編集したとき、ゲームを再起動しなくても自動で更新を検知して画面に反映させたいです!
🐻❄️ シロクマ先生
それが【アセット・ホットリロード】じゃ!`fs::last_write_time(path)` でファイルの最終更新タイムスタンプを定期監視すれば、ファイルが更新された瞬間にリロード処理をキックできるぞ!開発効率が何十倍にも跳ね上がる必殺技じゃ!
📄 AssetWatcher.cpp
#include <iostream>
#include <filesystem>
#include <chrono>
namespace fs = std::filesystem;
class AssetWatcher {
private:
fs::path targetPath_;
fs::file_time_type lastTime_;
public:
explicit AssetWatcher(fs::path path) : targetPath_(std::move(path)) {
std::error_code ec;
if (fs::exists(targetPath_, ec)) {
lastTime_ = fs::last_write_time(targetPath_, ec);
}
}
// 毎フレームまたは定期タイマーで呼び出して変更をチェック
bool checkReloadNeeded() {
std::error_code ec;
if (!fs::exists(targetPath_, ec)) return false;
auto currentTime = fs::last_write_time(targetPath_, ec);
if (!ec && currentTime != lastTime_) {
lastTime_ = currentTime; // タイムスタンプ更新
return true; // ホットリロード実行トリガー!
}
return false;
}
const fs::path& getPath() const { return targetPath_; }
};
int main() {
std::cout << "--- M9 アセットホットリロードウォッチャー ---" << std::endl;
AssetWatcher watcher("config/stage1.txt");
std::cout << "監視対象: " << watcher.getPath() << "\n";
std::cout << "ホットリロード監視機構の初期化成功\n";
return 0;
}
📝 理解度チェッククイズ
Q1. std::filesystem::path を使ってディレクトリパスとファイル名を結合する最も標準的で推奨される記法はどれですか?
- 1. fs::path p = "assets" + "\" + "textures" + "\" + "alien.png";
- 2. fs::path p = fs::path("assets") / "textures" / "alien.png";
- 3. fs::path p = fs::concat("assets", "textures", "alien.png");
- 4. fs::path p = sprintf("assets/%s/%s", "textures", "alien.png");
正解と解説を見る
正解:fs::path p = fs::path("assets") / "textures" / "alien.png";
std::filesystem::path はスラッシュ演算子(/)をオーバーロードしており、OSに応じた区切り文字(Windowsでは「\」、Unixでは「/」)を自動的に適切に補完・正規化してパスを結合します。
Q2. 指定ディレクトリ直下だけでなく、すべてのサブフォルダを再帰的に巡回してファイルを探すためのイテレータはどれですか?
- 1. std::filesystem::directory_iterator
- 2. std::filesystem::recursive_directory_iterator
- 3. std::filesystem::deep_file_iterator
- 4. std::filesystem::folder_crawler
正解と解説を見る
正解:std::filesystem::recursive_directory_iterator
directory_iterator は直下の要素のみを走査しますが、recursive_directory_iterator を使うとサブディレクトリの深層まで自動で再帰走査してくれます。
Q3. ゲーム開発などの例外(exception)を原則禁止したプロジェクトで、std::filesystem の関数が失敗した際に例外を投げさせないための正しい方法はどれですか?
- 1. 関数の前に noexcept キーワードを記述する
- 2. 関数の第2引数に std::error_code& を渡すオーバーロードを使用し、エラーコードの成否を判定する
- 3. 例外が発生したら std::terminate() で即座にプロセスを強制終了する
- 4. ファイル操作関数を呼び出す前にOSの生APIで毎回チェックする
正解と解説を見る
正解:関数の第2引数に std::error_code& を渡すオーバーロードを使用し、エラーコードの成否を判定する
std::filesystem のほとんどの関数には、例外を投げる版と、第2引数に std::error_code& を受け取って例外を一切投げない(noexcept保証)版の2種類が用意されており、ゲーム開発では後者が標準的に使われます。
🐻❄️ シロクマC++ラボ インタラクティブ学習システム
ブラウザでJavaScriptを有効にすると、ブラウザ内インベーダーゲームエミュレータ、メモリマップ可視化、UMLクラス図、対話型解説、理解度クイズが起動します。
インタラクティブ学習を開始する →