C++17から標準ライブラリに導入されたstd::filesystemライブラリは、ファイルシステム操作を劇的に簡略化しました。
その中でも、相対パスを絶対パスに変換する処理は頻繁に使用されますが、「どの関数を使えば正しい結果が得られるのか」という疑問を持つ開発者は少なくありません。
本記事では、絶対パスを取得するための中心的な機能であるstd::filesystem::canonicalとstd::filesystem::absoluteの違い、そして使い分けについて詳しく解説します。
C++におけるパス操作の重要性
現代のソフトウェア開発において、ファイルやディレクトリの場所を正確に特定することは、プログラムの信頼性を担保する上で極めて重要です。
特に、実行環境によってカレントディレクトリが異なる場合や、シンボリックリンクが多用される環境では、相対パスのままでは意図しないファイルを操作してしまうリスクがあります。

C++17以前では、OS固有のAPI(WindowsのGetFullPathNameやPOSIXのrealpath)を呼び出す必要があり、コードのポータビリティを損なう要因となっていました。
しかし、std::filesystemの登場により、OSに依存しない標準的な方法でパスを扱えるようになり、コードの保守性が飛躍的に向上しています。
std::filesystem::absoluteの基本
std::filesystem::absoluteは、与えられたパスを絶対パスに変換するための最も基本的な関数です。
この関数は非常にシンプルに動作し、基本的には「現在の作業ディレクトリ(CWD)」を相対パスの先頭に付与することで絶対パスを生成します。

absoluteの動作原理と特徴
std::filesystem::absoluteの最大の特徴は、「対象のファイルやディレクトリが実際に存在するかどうかをチェックしない」という点にあります。
このため、まだ作成されていないファイルのパスを事前に計算する場合などに適しています。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
// 存在しないファイルへのパス
fs::path p = "non_existent_file.txt";
// 絶対パスに変換
fs::path abs_p = fs::absolute(p);
// 結果の出力
std::cout << "Input: " << p << std::endl;
std::cout << "Absolute: " << abs_p << std::endl;
return 0;
}
実行結果の例(Windowsの場合):
Input: "non_existent_file.txt"
Absolute: "C:\\Users\\Username\\Projects\\TestApp\\non_existent_file.txt"
実行結果の例(Linuxの場合):
Input: "non_existent_file.txt"
Absolute: "/home/username/projects/testapp/non_existent_file.txt"
このように、ファイルが存在しなくてもエラーにならず、単純にカレントディレクトリと結合した結果を返します。
ただし、absoluteはパスの中に含まれる..(親ディレクトリ)や.(カレントディレクトリ)といった冗長な要素を完全に取り除かない場合がある点に注意が必要です。
std::filesystem::canonicalの基本
対して、std::filesystem::canonicalは、より厳密な絶対パスの取得を目的としています。
この関数は、単にパスを結合するだけでなく、パスに含まれるすべての要素を「正規化」し、シンボリックリンクもすべて解決します。

canonicalの厳密な制約
std::filesystem::canonicalを使用する際に最も重要なルールは、「対象となるパスが実際に存在しなければならない」という点です。
もし存在しないパスを渡した場合、この関数は例外(std::filesystem::filesystem_error)をスローします。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
try {
// カレントディレクトリにある既存のファイル
fs::path p = "existing_config.json";
// 正規化された絶対パスを取得
fs::path canon_p = fs::canonical(p);
std::cout << "Canonical: " << canon_p << std::endl;
} catch (const fs::filesystem_error& e) {
// ファイルが存在しない場合はここに来る
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
この厳密さにより、canonicalから得られたパスは常に一意であり、「そのパスがシステム上のどこを指しているか」を完全に保証することができます。
シンボリックリンクを経由している場合でも、リンク先の実際の実体パスを返してくれるため、セキュリティ上のチェックや重複排除に非常に役立ちます。
absoluteとcanonicalの決定的な違い
これら二つの関数は、一見似ていますがその役割は明確に異なります。
以下の表で、主な違いを比較してみましょう。
| 機能・特徴 | std::filesystem::absolute | std::filesystem::canonical |
|---|---|---|
| 存在チェック | 行わない | 行う(存在しないと例外) |
| ドット要素(./..) | 残る場合がある | 完全に削除・解決される |
| シンボリックリンク | リンクパスのまま保持 | リンク先の実体パスに解決 |
| 主な用途 | 新規作成予定のパス生成 | 既存ファイルの厳密な特定 |

冗長なパスの解決例
例えば、./a/../b.txtというパスがあった場合、absoluteではそのままの形で絶対パス化されることが多いですが、canonicalでは中間にあるa/..が計算され、直接b.txtを指すクリーンなパスになります。
#include <iostream>
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
int main() {
// テスト用のディレクトリとファイルを作成
fs::create_directory("test_dir");
std::ofstream("test_dir/file.txt").close();
// 冗長な相対パス
fs::path p = "test_dir/../test_dir/file.txt";
std::cout << "Absolute: " << fs::absolute(p) << std::endl;
std::cout << "Canonical: " << fs::canonical(p) << std::endl;
// 後片付け
fs::remove_all("test_dir");
return 0;
}
Absolute: "C:\\Project\\test_dir\\..\\test_dir\\file.txt"
Canonical: "C:\\Project\\test_dir\\file.txt"
このように、canonicalは「人が見てわかりやすく、コンピュータにとって一意なパス」を提供します。
weakly_canonicalという選択肢
canonicalは便利ですが、「ファイルが存在しないとエラーになる」という制約が厳しすぎることがあります。
「基本的には正規化したいが、最後のファイル名だけはまだ存在しなくても許容したい」というケースです。
このような場合に便利なのがstd::filesystem::weakly_canonicalです。
weakly_canonicalの動作
この関数は、パスを先頭から順に辿っていき、「存在する部分まではcanonicalとして解決し、存在しない部分はそのまま結合する」という賢い振る舞いをします。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
// "existing_dir" は存在するが、"new_file.txt" は存在しない場合
fs::create_directory("existing_dir");
fs::path p = "existing_dir/../existing_dir/new_file.txt";
// canonicalだと例外が出るが、weakly_canonicalなら通る
std::cout << "Weakly Canonical: " << fs::weakly_canonical(p) << std::endl;
fs::remove_all("existing_dir");
return 0;
}
Weakly Canonical: "C:\\Project\\existing_dir\\new_file.txt"
これにより、「存在する親ディレクトリまでは正規化しつつ、新しいファイルのパスを綺麗に生成する」ことが可能になります。
実践的なエラーハンドリング
canonicalを使用する際は、例外処理を忘れてはいけません。
しかし、C++の例外機構を使いたくない、あるいはパフォーマンスを重視したい場合は、std::error_codeを受け取るオーバーロードを使用します。
#include <iostream>
#include <filesystem>
#include <system_error>
namespace fs = std::filesystem;
int main() {
fs::path p = "non_existent.txt";
std::error_code ec;
fs::path canon_p = fs::canonical(p, ec);
if (ec) {
// エラーが発生した場合の処理
std::cerr << "Error Code: " << ec.value() << std::endl;
std::cerr << "Message: " << ec.message() << std::endl;
} else {
std::cout << "Path: " << canon_p << std::endl;
}
return 0;
}
この方法を用いることで、プログラムの制御フローを乱すことなく、安全にパスの存在確認と絶対パス取得を同時に行うことができます。
まとめ
C++で絶対パスを取得する際、absoluteとcanonicalの使い分けは非常に明確です。
- absolute:単にカレントディレクトリを付け足したいときや、ファイルが存在しないことが前提のときに使用します。
- canonical:既存のファイルを厳密に特定したいとき、シンボリックリンクを解決したいとき、あるいは「..」などを取り除いた最短パスが欲しいときに使用します。
- weakly_canonical:一部が存在しないパスに対して、可能な限り正規化を行いたいときに使用します。
これらの関数を適切に選択することで、ファイル操作にまつわるバグを未然に防ぎ、堅牢なアプリケーションを構築することができます。
パス操作は一見地味なトピックですが、ファイルシステムという外部環境と対話するプログラムにおいては、「正確な場所を知ること」こそが全ての処理の出発点となるのです。
