C++17以前、プログラムからファイルやディレクトリを操作するのは非常に手間のかかる作業でした。
プラットフォームごとに異なるAPI(WindowsならWin32 API、UNIX系ならPOSIXなど)を使い分ける必要があり、コードのポータビリティを確保するのが困難だったからです。
しかし、C++17で導入されたfilesystemライブラリによって、この状況は一変しました。
標準ライブラリだけで、OSの違いを意識することなく、直感的かつ強力なファイル操作が可能になったのです。
本記事では、この便利なfilesystemの使い方を、基礎から応用まで徹底的に解説します。
filesystemライブラリの基本準備
filesystemを利用するには、まず適切なヘッダーをインクルードし、名前空間を整理することから始めます。
このライブラリはstd::filesystemという名前空間に定義されていますが、コードを短く記述するためにエイリアス(別名)を利用するのが一般的です。

ヘッダーのインクルードと名前空間
まずは基本的なコードの枠組みを確認しましょう。
C++17以降をサポートするコンパイラであれば、特別なライブラリのインストールなしに使用できます。
#include <iostream>
#include <filesystem> // filesystemライブラリをインクルード
// 長い名前空間を「fs」という短い名前に置き換えるのが一般的です
namespace fs = std::filesystem;
int main() {
// ここに処理を記述していきます
return 0;
}
名前空間のエイリアス設定は、コードの可読性を高めるために非常に重要です。
std::filesystem::pathと毎回書くよりも、fs::pathと書くほうがスッキリとして見通しが良くなります。
pathクラス:ファイルパスの抽象化
filesystemライブラリの中心となるのがpathクラスです。
これは単なる文字列ではなく、ファイルシステム上の場所を表現するための専用オブジェクトです。

パスオブジェクトの生成と基本操作
fs::pathは、文字列から簡単に作成できます。
OSごとのディレクトリ区切り文字(Windowsならバックスラッシュ、Linuxならスラッシュ)を自動的に適切に処理してくれるのが最大のメリットです。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
// パスオブジェクトの生成
fs::path p = "data/logs/example.txt";
// 各要素の抽出
std::cout << "フルパス: " << p << std::endl;
std::cout << "親ディレクトリ: " << p.parent_path() << std::endl;
std::cout << "ファイル名: " << p.filename() << std::endl;
std::cout << "拡張子なしのファイル名: " << p.stem() << std::endl;
std::cout << "拡張子: " << p.extension() << std::endl;
return 0;
}
フルパス: "data/logs/example.txt"
親ディレクトリ: "data/logs"
ファイル名: "example.txt"
拡張子なしのファイル名: "example"
拡張子: ".txt"
パスの結合と正規化
パスを結合する場合、文字列として「+」で繋ぐと区切り文字の扱いに困ることがあります。
しかし、pathクラスの/=演算子や/演算子を使えば、安全かつ簡単にパスを結合できます。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path base = "C:/Users/Admin";
fs::path sub = "Documents";
fs::path file = "note.txt";
// パスの結合
fs::path full_path = base / sub / file;
std::cout << "結合されたパス: " << full_path << std::endl;
return 0;
}
結合されたパス: "C:/Users/Admin/Documents/note.txt"
このように、/演算子を使うだけで、内部的に適切な区切り文字を挿入してくれます。
これはクロスプラットフォーム開発において非常に強力な機能です。
ファイルとディレクトリの基本操作
パスの扱い方を理解したら、次は実際のファイルシステムへの操作(作成、削除、コピーなど)を見ていきましょう。

ディレクトリの作成
ディレクトリを作成するには、fs::create_directoryまたはfs::create_directoriesを使用します。
create_directory:指定したディレクトリを1つ作成します。親ディレクトリが存在しない場合は失敗します。create_directories:深い階層のディレクトリを一括で作成します。途中の階層がなくても自動的に補完してくれます。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path dir_path = "output/logs/2026/january";
// 中間ディレクトリを含めて作成
if (fs::create_directories(dir_path)) {
std::cout << "ディレクトリを作成しました。" << std::endl;
} else {
std::cout << "ディレクトリは既に存在するか、作成に失敗しました。" << std::endl;
}
return 0;
}
存在確認とファイル情報の取得
ファイルやディレクトリが実際に存在するかどうか、またそれがディレクトリなのかファイルなのかを判定するのは、最も頻繁に行われる処理の一つです。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path p = "example.txt";
// 存在確認
if (fs::exists(p)) {
// 種類判定
if (fs::is_regular_file(p)) {
std::cout << "これは通常のファイルです。サイズ: " << fs::file_size(p) << " bytes" << std::endl;
} else if (fs::is_directory(p)) {
std::cout << "これはディレクトリです。" << std::endl;
}
} else {
std::cout << "対象が存在しません。" << std::endl;
}
return 0;
}
注意点として、fs::file_sizeをディレクトリに対して呼び出すと例外が発生します。
必ず事前にis_regular_fileで確認するようにしましょう。
ファイルのコピーと移動
ファイルの複製や移動も非常にシンプルです。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path src = "source.txt";
fs::path dst = "backup.txt";
try {
// ファイルのコピー
// 第3引数にオプションを指定可能(既存なら上書きなど)
fs::copy(src, dst, fs::copy_options::overwrite_existing);
std::cout << "コピーが完了しました。" << std::endl;
// 名前の変更(移動)
fs::rename(dst, "backup_old.txt");
std::cout << "リネームが完了しました。" << std::endl;
} catch (const fs::filesystem_error& e) {
std::cerr << "エラーが発生しました: " << e.what() << std::endl;
}
return 0;
}
削除操作
削除にはfs::removeとfs::remove_allがあります。
fs::remove:空のディレクトリまたはファイルを削除します。中身があるディレクトリを指定するとエラーになります。fs::remove_all:中身を含めて再帰的にすべて削除します。非常に強力なため、使用には注意が必要です。
ディレクトリの探索(イテレーション)
ディレクトリ内のファイル一覧を取得する処理は、filesystemライブラリの真骨頂とも言える部分です。

直下のファイル一覧を取得
fs::directory_iteratorを使うと、範囲ベースのforループで簡単にファイル一覧を処理できます。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path target_dir = "."; // カレントディレクトリ
std::cout << "--- ファイル一覧 ---" << std::endl;
for (const auto& entry : fs::directory_iterator(target_dir)) {
// entryは directory_entry オブジェクト
std::cout << entry.path().filename() << std::endl;
}
return 0;
}
サブディレクトリも含めた再帰的探索
サブディレクトリの中身まで全てスキャンしたい場合は、fs::recursive_directory_iteratorを使用します。
たった一行の変更で、階層構造をすべて網羅する探索が可能になります。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path target_dir = "projects";
std::cout << "--- 全ファイルを再帰的に探索 ---" << std::endl;
for (const auto& entry : fs::recursive_directory_iterator(target_dir)) {
// ディレクトリの場合はスキップするなどの処理も可能
if (fs::is_regular_file(entry)) {
std::cout << "File: " << entry.path() << std::endl;
}
}
return 0;
}
このイテレータは非常に賢く、シンボリックリンクによる無限ループを避ける設定などもオプションで行うことができます。
エラーハンドリングの2つの手法
filesystemの関数には、大きく分けて2種類のエラー通知方法があります。
例外によるハンドリング
これまで見てきた例のように、エラー発生時にfs::filesystem_errorを投げる形式です。
try {
fs::create_directory("/invalid/path");
} catch (const fs::filesystem_error& e) {
// 権限不足やパス不正などでここに来る
std::cout << "エラーメッセージ: " << e.what() << std::endl;
}
std::error_codeによるハンドリング
例外を投げたくない場合、関数の引数にstd::error_codeを渡すオーバーロードを使用します。
これを利用すると、プログラムの実行を中断させずにエラー処理を行えます。
#include <system_error>
std::error_code ec;
fs::create_directory("/invalid/path", ec);
if (ec) {
std::cout << "エラーが発生しました: " << ec.message() << std::endl;
}
パフォーマンスが要求されるループ内での処理や、エラーが発生しても処理を続行したい場合には、こちらの方法が適しています。
実践的な活用例:古いファイルの自動クリーンアップ
これまでの知識を組み合わせて、実用的なプログラムを作成してみましょう。
特定のディレクトリにあるファイルのうち、最終更新日時が一定期間(例えば30日)を過ぎたものを削除するプログラムのイメージです。

#include <iostream>
#include <filesystem>
#include <chrono>
namespace fs = std::filesystem;
using namespace std::chrono_literals;
int main() {
fs::path log_dir = "logs";
auto now = std::chrono::file_clock::now();
auto threshold = 24h * 30; // 30日分
if (!fs::exists(log_dir)) return 0;
for (const auto& entry : fs::directory_iterator(log_dir)) {
auto last_time = fs::last_write_time(entry);
// 経過時間を計算
if (now - last_time > threshold) {
std::cout << "削除中: " << entry.path().filename() << std::endl;
fs::remove(entry.path());
}
}
return 0;
}
このコードでは、fs::last_write_timeを使用してファイルの最終更新日時を取得し、chronoライブラリと組み合わせて時間判定を行っています。
C++17以降、このような複雑なファイル管理システムも、標準ライブラリだけで極めて簡潔に記述できるようになりました。
まとめ
C++17のfilesystemライブラリは、モダンなC++開発において必須のスキルと言えます。
pathクラスによるプラットフォーム非依存のパス操作、直感的なディレクトリ探索、そして詳細なファイル情報の取得など、これまで開発者を悩ませてきたOS固有の壁を完全に取り払ってくれました。
最後に、利用時のポイントをまとめます。
| 項目 | 内容 |
|---|---|
| パス操作 | fs::path と / 演算子を使い、OSの区切り文字を意識しない。 |
| 安全性 | ファイル情報の取得前には fs::exists で確認する。 |
| 探索 | recursive_directory_iterator で深い階層も一発スキャン。 |
| エラー | 実行を止めたくない場合は std::error_code を活用する。 |
これらの機能を使いこなすことで、あなたのC++コードはよりポータブルで、メンテナンス性の高いものになるでしょう。
ぜひ、実際のプロジェクトでファイル操作を実装する際には、この強力な標準ライブラリを最大限に活用してください。
