C++でプログラムを開発する際、設定ファイルやログファイル、ユーザーが指定したデータファイルなどが実際にストレージ上に存在するかどうかを確認する処理は非常に頻繁に発生します。
かつてのC++では、OS固有のAPIを使用したり、標準入出力ライブラリの挙動を利用したりといった、やや回りくどい方法が必要でした。
しかし、C++17で導入されたstd::filesystemライブラリによって、このファイル存在確認は驚くほど簡単かつ標準的な方法で記述できるようになりました。
本記事では、現代的なC++におけるファイル存在確認の決定版であるstd::filesystem::existsの使い方を中心に、エラーハンドリングや注意点、さらにはレガシーな手法との比較までを詳しく解説します。
std::filesystem::existsの基本概念
C++17以降、ファイルシステムを操作するための標準的な手段としてstd::filesystemが追加されました。
このライブラリの中に含まれるexists関数を使用することで、特定のパスにファイルやディレクトリが存在するかを判定できます。

基本的な使い方とヘッダーのインポート
std::filesystem::existsを使用するには、<filesystem>ヘッダーをインクルードする必要があります。
また、型名や関数名が長くなりがちなため、namespace fs = std::filesystem;のようにエイリアスを作成して記述するのが一般的です。
以下のコードは、指定したファイル名が存在するかどうかを確認する最もシンプルな例です。
#include <iostream>
#include <filesystem> // std::filesystemを使用するために必要
// 名前空間のエイリアスを作成して記述を簡略化する
namespace fs = std::filesystem;
int main() {
// 確認したいファイルパスを指定
fs::path filePath = "example.txt";
// exists関数で存在確認を行う
if (fs::exists(filePath)) {
std::cout << "ファイルまたはディレクトリが存在します。" << std::endl;
} else {
std::cout << "ファイルまたはディレクトリは見つかりませんでした。" << std::endl;
}
return 0;
}
ファイルまたはディレクトリは見つかりませんでした。
※カレントディレクトリに「example.txt」が存在しない場合の出力です。
exists関数の引数と戻り値
exists関数は、std::filesystem::pathオブジェクト、あるいは文字列などのパスとして解釈可能なものを受け取ります。
戻り値はbool型であり、ファイルが存在すればtrue、存在しなければfalseを返します。
ここで注意したいのは、この関数はファイルとディレクトリのどちらが存在してもtrueを返すという点です。
もし「ファイルとして存在するか」あるいは「ディレクトリとして存在するか」を厳密に区別したい場合は、後述する別の関数を組み合わせる必要があります。
ファイルとディレクトリを区別する方法
existsはパスの有無だけを判定するため、そのパスが通常のファイルなのか、あるいはフォルダ(ディレクトリ)なのかまでは教えてくれません。
これらを区別するためには、is_regular_fileやis_directoryといった関数を併用します。

is_regular_fileとis_directoryの使い方
特定のパスが「存在し、かつ通常のファイルであること」を確認したい場合は、fs::is_regular_fileを使用します。
同様に、ディレクトリであることを確認したい場合はfs::is_directoryを使用します。
#include <iostream>
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
int main() {
// テスト用のファイルとディレクトリを作成
fs::create_directory("test_dir");
std::ofstream("test_dir/test_file.txt");
fs::path p1 = "test_dir";
fs::path p2 = "test_dir/test_file.txt";
// ディレクトリの判定
if (fs::exists(p1) && fs::is_directory(p1)) {
std::cout << p1 << " はディレクトリです。" << std::endl;
}
// 通常ファイルの判定
if (fs::is_regular_file(p2)) {
// is_regular_file自体が存在確認も含んでいるため、existsの併用は必須ではない
std::cout << p2 << " は通常のファイルです。" << std::endl;
}
return 0;
}
"test_dir" はディレクトリです。
"test_dir/test_file.txt" は通常のファイルです。
is_regular_fileなどは、対象が存在しない場合にはfalseを返します。
そのため、わざわざexistsでチェックしてからis_regular_fileを呼ぶ必要はありません。
ファイルとしてアクセスしたいのであれば、最初からis_regular_fileを使うのがスマートです。
エラーハンドリングと例外処理
std::filesystem::existsには、大きく分けて二つのオーバーロードが存在します。
一つは異常時に例外を投げるもの、もう一つはエラーコードを引数として受け取るものです。
例外を投げるデフォルトの挙動
通常、fs::exists(path)のように呼び出した場合、OSレベルでの致命的なエラー(アクセス権限がない、パスが長すぎるなど)が発生すると、std::filesystem::filesystem_errorという例外がスローされます。
try {
if (fs::exists("/restricted_access_path")) {
// 処理
}
} catch (const fs::filesystem_error& e) {
std::cerr << "エラーが発生しました: " << e.what() << std::endl;
}
エラーコードを使用する非例外的な挙動
プログラムの実行速度を重視する場合や、例外をキャッチする手間を省きたい場合には、std::error_codeを引数に渡す方法が推奨されます。
この場合、エラーが発生しても例外はスローされず、エラーの詳細が引数のオブジェクトに格納されます。
#include <iostream>
#include <filesystem>
#include <system_error>
namespace fs = std::filesystem;
int main() {
fs::path p = "/config.ini";
std::error_code ec;
if (fs::exists(p, ec)) {
std::cout << "パスが存在します。" << std::endl;
} else {
if (ec) {
// エラーが発生して存在しないと判定された場合
std::cerr << "OSエラー: " << ec.message() << std::endl;
} else {
// 純粋にファイルが見つからない場合
std::cout << "ファイルは見つかりませんでした。" << std::endl;
}
}
return 0;
}

シンボリックリンクの扱い
ファイルシステムを扱う上で避けて通れないのがシンボリックリンク(ショートカットのようなもの)の存在です。
exists関数はデフォルトで、シンボリックリンクの「リンク先」が存在するかどうかをチェックします。
リンク自体をチェックするか、先をチェックするか
もしリンク先が削除されている(壊れたリンク)場合、リンクファイル自体は存在していてもexistsはfalseを返します。
もしリンクそのものの存在を確認したい場合は、symlink_statusを使用する必要があります。
| 関数名 | 対象 | 特徴 |
|---|---|---|
exists(p) | リンク先 | リンク先の実体が存在する場合にのみtrueを返す |
is_symlink(p) | リンク自体 | 指定されたパスがシンボリックリンクであればtrueを返す |
status(p) | リンク先 | ファイル属性を取得する際にリンクを解決する |
symlink_status(p) | リンク自体 | リンクを解決せずにファイル属性を取得する |
このように、リンクが絡む環境ではexistsの結果が直感的でない場合があるため注意が必要です。
従来の古い方法(C++11以前)との比較
C++17が利用できない古い環境や、レガシーなコードを保守している場合、std::filesystem以外の方法が使われていることがあります。
それぞれの特徴を知っておくことで、リファクタリングの際に役立ちます。
1. std::ifstream を使用する方法
ファイルを開こうとしてみて、成功するかどうかで存在を確認する手法です。
#include <fstream>
bool exists_old_school(const std::string& name) {
std::ifstream f(name.c_str());
return f.good();
}
この方法はシンプルですが、存在を確認するためだけにファイルを開く処理(システムコール)を伴うため、パフォーマンス面で不利です。
また、読み取り権限がないだけで「存在しない」と誤認してしまうリスクもあります。
2. POSIXの stat 関数を使用する方法
C言語由来の非常に高速な方法ですが、OSごとに挙動が異なる場合があり、C++らしい書き方とは言えません。
| 手法 | 可搬性 (Portability) | 速度 | 推奨度 |
|---|---|---|---|
std::filesystem::exists | 高い (標準) | 高速 | 最高 |
std::ifstream::good | 高い | 低速 | 低い |
POSIX stat | 低い (Unix系) | 最速 | 中 (特定環境) |
実践的な注意点とベストプラクティス
ファイル存在確認をプログラムに組み込む際、単に「関数を呼べば終わり」ではない、実務上の落とし穴がいくつか存在します。
TOCTOU問題 (タイム・オブ・チェック・トゥ・タイム・オブ・ユース)
これはセキュリティや並列処理に関わる重要な概念です。
「存在を確認してから、実際にファイルを開くまでのわずかな時間」に、別のプロセスがそのファイルを削除したり変更したりする可能性があります。

この問題を回避するためには、「存在を確認してから開く」のではなく、「最初から開こうと試みて、失敗したときのエラー内容を確認する」という設計の方が安全な場合があります。
パスの正規化
相対パス(例: ../data.txt)を使用する場合、プログラムの「カレントディレクトリ」がどこになっているかに依存します。
意図しない結果を防ぐために、fs::absolute関数を使用して絶対パスに変換してから確認作業を行うのも一つの手です。
fs::path relativePath = "config.xml";
fs::path absolutePath = fs::absolute(relativePath);
if (fs::exists(absolutePath)) {
// 確実に存在する絶対パスで処理
}
まとめ
C++17以降の標準となったstd::filesystem::existsは、ファイル存在確認における最も強力で簡潔な手段です。
かつてのようなOS依存のコードや、ifstreamを利用した非効率な確認は、現代のC++開発ではこの新しいライブラリに置き換えられるべきでしょう。
本記事のポイントを振り返ると以下の通りです。
existsはファイルとディレクトリの両方に反応する。- 厳密な区別が必要なら
is_regular_fileなどを使用する。 - パフォーマンスや安全性を重視する場合は
std::error_codeを活用し、例外を制御する。 - TOCTOU問題などの競合状態には常に注意を払う。
これらの知識を活かして、より堅牢で移植性の高いファイル操作処理を実装してください。
C++の標準ライブラリは進化を続けており、これらを正しく使いこなすことが、モダンなプログラミングへの第一歩となります。
