閉じる

C++でファイルサイズを取得する方法:filesystemから従来手法まで

C++でプログラムを開発する際、ファイルサイズを取得する処理は非常に頻繁に発生します。

例えば、ファイルをメモリに読み込むためのバッファサイズを決定したり、ディスク容量をチェックしたり、あるいはダウンロードの進捗を表示したりといった場面です。

かつてのC++ではOS固有のAPIやトリッキーな手法が使われてきましたが、現在のC++では標準ライブラリのみで完結する安全かつ効率的な方法が確立されています。

本記事では、最新のC++17以降の標準手法から、古い環境でも動作する従来の手法までを詳しく解説します。

モダンC++における標準手法:std::filesystem

C++17において、ファイルシステムを操作するための標準ライブラリstd::filesystemが導入されました。

これにより、プラットフォームに依存することなく、非常にシンプルなコードでファイルサイズを取得できるようになりました。

現在の開発において、特別な理由がない限りはこの手法を採用するのが最も賢明です。

std::filesystem::file_size の使い方

std::filesystem::file_size関数は、引数にファイルパス(std::filesystem::path)を渡すだけで、そのサイズをuintmax_t型で返してくれます。

この型は、そのプラットフォームで扱える最大の整数型であり、非常に大きなファイルサイズにも対応可能です。

C++
#include <iostream>
#include <filesystem> // C++17から導入
#include <fstream>

namespace fs = std::filesystem;

int main() {
    // テスト用のファイルを作成
    const std::string filename = "test_file.txt";
    std::ofstream ofs(filename);
    ofs << "Hello, C++ FileSystem!";
    ofs.close();

    try {
        // ファイルパスをオブジェクト化
        fs::path p(filename);

        // ファイルサイズを取得(バイト単位)
        // 戻り値は std::uintmax_t 型
        std::uintmax_t size = fs::file_size(p);

        std::cout << "ファイル名: " << p.filename() << std::endl;
        std::cout << "サイズ: " << size << " バイト" << std::endl;
    } catch (fs::filesystem_error& e) {
        // ファイルが存在しない場合などは例外が発生する
        std::cerr << "エラー: " << e.what() << std::endl;
    }

    return 0;
}
実行結果
ファイル名: "test_file.txt"
サイズ: 23 バイト

例外処理とエラーハンドリング

上記のコードではtry-catchブロックを使用していますが、ファイルが存在しない場合に例外を投げたくない場合は、第2引数にstd::error_codeを渡すオーバーロードを使用します。

これにより、例外処理のオーバーヘッドを避け、よりC言語に近いエラーチェックが可能になります。

C++
std::error_code ec;
auto size = fs::file_size("non_existent.txt", ec);

if (ec) {
    std::cout << "エラー発生: " << ec.message() << std::endl;
} else {
    std::cout << "サイズ: " << size << std::endl;
}

メリットと注意点

std::filesystemを利用する最大のメリットは、コードの可読性が圧倒的に高いことです。

また、パスの結合やディレクトリ作成といった他の操作と親和性が高い点も魅力です。

一方で注意点として、シンボリックリンクを指している場合に、「リンク自体のサイズ」ではなく「リンク先のファイルサイズ」を返す点が挙げられます。

また、ディレクトリに対してこの関数を呼び出すとエラー(例外)になるため、事前に対象が通常のファイル(is_regular_file)であるかを確認することを推奨します。

汎用的なストリーム手法:std::ifstream

C++17以前から広く使われてきた手法として、標準入出力ストリームであるstd::ifstreamを利用する方法があります。

ファイルを開き、読み取り位置(ファイルポインタ)を末尾に移動させ、その位置を取得することでサイズを測定します。

seekgとtellgによる取得

この手法は、seekgで読み取り位置を末尾(std::ios::end)に移動させ、tellgで現在のバイト位置を取得するという手順を踏みます。

C++
#include <iostream>
#include <fstream>
#include <string>

std::streamsize get_file_size_via_stream(const std::string& filename) {
    // 必ずバイナリモード(std::ios::binary)で開くこと
    std::ifstream file(filename, std::ios::binary | std::ios::ate);

    if (!file.is_open()) {
        return -1;
    }

    // std::ios::ateを指定して開くと、最初から末尾にポインタがある
    // そのため、そのままtellg()を呼べばサイズが取得できる
    std::streamsize size = file.tellg();
    
    return size;
}

int main() {
    std::string path = "example.dat";
    std::streamsize size = get_file_size_via_stream(path);

    if (size != -1) {
        std::cout << "ファイルサイズ: " << size << " バイト" << std::endl;
    } else {
        std::cout << "ファイルを開けませんでした。" << std::endl;
    }

    return 0;
}
実行結果
ファイルサイズ: 1024 バイト

バイナリモードの重要性

この手法において最も重要なのは、ファイルをバイナリモード(std::ios::binary)で開くことです。

テキストモードで開いた場合、OSによっては改行コード(CRLFからLFへの変換など)が自動的に行われるため、tellgが返す値が実際のディスク上のファイルサイズと一致しなくなる可能性があります。

性能面での考慮事項

std::ifstreamは汎用性が高い反面、ファイルサイズを取得するためだけにファイル全体を「オープン」する動作が伴います。

ファイルサイズだけを高速に、大量のファイルに対して取得したい場合には、次に紹介するOSレベルのAPIに近い手法の方が効率的です。

C言語由来の高速手法:stat構造体

C++はC言語との互換性を持っているため、C標準ライブラリのstat関数を使用することも可能です。

この方法は、ファイルの中身を読み取るための「オープン」処理を完全に行わず、ファイルシステムのメタデータのみを参照するため、非常に高速に動作します。

sys/stat.h を利用した実装

POSIX準拠のシステム(LinuxやmacOS)およびWindows(MSVC)の多くで利用可能な方法です。

C++
#include <iostream>
#include <sys/stat.h>
#include <string>

long get_file_size_stat(const std::string& filename) {
    struct stat stat_buf;
    
    // stat関数にファイル名と構造体のアドレスを渡す
    // 成功すれば0を返す
    int rc = stat(filename.c_str(), &stat_buf);
    
    return rc == 0 ? stat_buf.st_size : -1;
}

int main() {
    const char* path = "test_image.png";
    long size = get_file_size_stat(path);

    if (size != -1) {
        std::cout << "ファイルサイズ(stat): " << size << " バイト" << std::endl;
    } else {
        std::perror("statエラー");
    }

    return 0;
}
実行結果
ファイルサイズ(stat): 45678 バイト

32bit/64bitの壁

古いシステムや特定のコンパイラ設定では、struct statst_sizeが32ビット整数(long)である場合があります。

この場合、2GBを超えるファイルサイズを正しく取得できないという問題が発生します。

現代の環境では、struct _stat64_stat64()関数を使用することで、4GBを超える巨大なファイルにも対応可能です。

ポータビリティを重視するなら、やはりstd::filesystemに軍配が上がります。

OS固有のAPIによる最適化

特定のプラットフォームに特化して最大のパフォーマンスを引き出したい場合、OS固有のAPIを直接叩く手法があります。

これはクロスプラットフォーム開発には向きませんが、システムプログラミングにおいては重要な選択肢です。

Windows API (GetFileSizeEx)

Windows環境では、GetFileSizeExを使用するのが最も確実です。

これはHANDLE(ファイルハンドル)を対象とするため、既にファイルを開いている場合に非常に効率的です。

C++
#include <windows.h>
#include <iostream>

void print_windows_file_size(const char* filename) {
    // ファイルハンドルを取得
    HANDLE hFile = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL,
                               OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    if (hFile != INVALID_HANDLE_VALUE) {
        LARGE_INTEGER size;
        // 64ビット対応のサイズ取得関数
        if (GetFileSizeEx(hFile, &size)) {
            std::cout << "Windows API Size: " << size.QuadPart << " バイト" << std::endl;
        }
        CloseHandle(hFile);
    }
}

各手法の比較と使い分け

これまでに紹介した手法を、用途別に比較しました。

手法推奨される状況メリットデメリット
std::filesystem現代的な全プロジェクト標準機能、簡潔、安全、64bit対応C++17以降が必要
std::ifstream古いC++規格、ファイル処理のついで追加ライブラリ不要、直感的速度がやや遅い、バイナリモード必須
stat性能重視、組み込み、C互換非常に高速、オープン不要32/64bitの型に注意が必要
OS固有API特定OS専用、極限の最適化OSの機能をフル活用移植性がない、コードが複雑

どの方法を選ぶべきか

結論として、C++17以降が使える環境であれば「std::filesystem」一択です。

これは最もエラーが少なく、将来にわたって保守しやすいコードになります。

一方、古いレガシーなシステムや、どうしても実行バイナリのサイズを極限まで小さくしたい組み込み環境では、statを使用するのが一般的です。

まとめ

C++でファイルサイズを取得する方法は、言語の進化とともに洗練されてきました。

かつてはOSごとに異なるヘッダファイルを読み込み、巨大なファイルへの対応に苦慮していましたが、現代のC++では「std::filesystem::file_size」という標準的な解法が存在します。

もしあなたが新しいプロジェクトを開始するのであれば、まずはstd::filesystemを検討してください。

特定のパフォーマンスボトルネックが判明した際や、古いコンパイラに縛られている場合にのみ、statifstreamといった従来の手法を検討するのが、現代的なC++開発のベストプラクティスです。

各手法の特性を理解し、プロジェクトの要件に合わせて最適な道具を選択しましょう。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!