ランダムアクセス | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要

このページでは、ファイル内を自由に移動しながら読み書きを行うランダムアクセスという方法を取り上げます。また、これに関連する話題として、ファイルの末尾への追記、読み書き両用モード、そしてファイルサイズを取得する方法を紹介しています。バイナリエディタのプログラムも、このページで完成させます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



ランダムアクセス

関数ポインタとラムダ式」の練習問題で、バイナリエディタのプログラムに –sizeオプションを実装しました。基本的な動作は実現できていますが、実際のファイルサイズを越えた指定をされたときに、あたかもデータが存在しているかのような結果を出力してしまう問題が残っています。この問題を解決するためには、実際のファイルサイズまでで止める必要がありますが、そのためにファイルサイズを調べる方法が必要です。このページでその方法を取り上げます。

また、新たなオプションとして、–beginオプションの実装を目指します。これは、“–begin 1000” のように指定すると、ファイルの 1000バイト目から出力を始めるという機能です。この実装のために、ファイルのランダムアクセス (random access) についても取り上げていきます。

ファイルのランダムアクセスとは、ファイル内の目的の位置に一気に移動して読み書きを行うアクセス方法のことです。必要があれば、一度読み書きを行った位置に戻るような動きも可能です。ランダムアクセスに対して、これまでのページで行ってきたような、先頭から順番に読み書きを進めていく方法は、シーケンシャルアクセス (sequential access) と呼びます。

ファイルポジション

std::ifstream や std::ofstream などは、いまファイル内のどこを対象に処理を行っているかを管理しています。この位置情報を、ファイルポジション (file position) やストリームポジション (stream position) などと呼びます。

通常、ファイルをオープンした時点ではファイルポジションはファイルの先頭です。読み書きは、現在のファイルポジションに対して行われ、読み書きのあとには読み書きしたデータの大きさの分だけファイルポジションは移動します。そのため、先頭から末尾まで順番に読み書きするだけのシーケンシャルアクセスでは、ファイルポジションのことを特に意識せずともうまくいきます。

バイナリ形式での読み書き」のページでオープンモードの種類を取り上げたとき、std::ios_base::ate というものがありましたが、この指定があると、ファイルポジションはファイルの末尾から始まります。また、std::ios_base::app を指定した場合には、書き込みを行うたびに末尾に移動するという特殊な動作になります(あとで取り上げます)。

std::ios_base::ate は、ファイルをオープンした直後に、末尾へシーク(後述)することと同義です1

現在のファイルポジションは、tellgメンバ関数2tellpメンバ関数3を使って取得できます。tellgメンバ関数は読み取りのファイルポジション、tellpメンバ関数は書き込みのファイルポジションを返します。ただし、failメンバ関数が true を返す状態のときに呼び出した場合は、-1 が返されます。戻り値の型は std::ifstream::pos_typestd::ofstream::pos_type という型です。仕様が複雑なので詳細は省きますが、符号付き整数型のように使うことができて、ファイルポジションを表現するのに十分な大きさをもった型です4

tellg の gget、tellp の pput のことです。

重要な点として、イメージどおりの値(つまり、ファイルの先頭からの距離、バイト数)でファイルポジションを取得するには、ファイルをバイナリ形式でオープンしなければなりません。バイナリ形式での読み書き」のページで解説したとおり、テキスト形式でオープンすると、“¥r¥n” を “¥n” に置き換えるなどの変換が行われるからです。この変換のせいで、ファイルポジションが、実際にファイルの中身に対してずれたところにあるように見えることになります。

テキスト形式の場合でも、取得した値を、後述するシークの関数に渡す使い方は可能です。


次のサンプルプログラムは、ファイルをオープンした直後のファイルポジションと、4バイト分のデータを読み書きしたあとのファイルポジションを、std::ios_base::ate を指定しない場合と指定した場合のそれぞれで試しています(エラーチェックは省いています)。

#include <fstream>
#include <iostream>

static void tellp_test(const std::string& path, std::ios_base::openmode openmode)
{
    std::ofstream ofs(path, openmode);
    std::cout << ofs.tellp() << "\n";

    char s[] {"xyz"};
    ofs.write(s, sizeof(s));
    std::cout << ofs.tellp() << "\n";

    ofs.close();
}

static void tellg_test(const std::string& path, std::ios_base::openmode openmode)
{
    std::ifstream ifs(path, openmode);
    std::cout << ifs.tellg() << "\n";

    char s[4] {};
    ifs.read(s, sizeof(s));
    std::cout << ifs.tellg() << "\n";

    ifs.close();
}

int main()
{
    constexpr auto path = "test.bin";

    tellp_test(path, std::ios_base::out | std::ios_base::binary);
    tellg_test(path, std::ios_base::in | std::ios_base::binary);

    tellp_test(path, std::ios_base::out | std::ios_base::binary | std::ios_base::ate);
    tellg_test(path, std::ios_base::in | std::ios_base::binary | std::ios_base::ate);
}

実行結果:

0
4
0
4
0
4
4
-1

std::ios_base::ate を使った読み込みのとき、-1 が返されています。これはファイルの末尾まで読み取ったあと、さらに読み取りを行ったためです。

ファイル以外のストリーム

ファイルポジションと呼んでいるものの、実際には、std::ostringstream などの文字列ストリームや、std::cin などの標準入出力ストリームでも同様の考え方があります。

#include <iostream>
#include <sstream>
#include <string>

int main()
{
    std::string s {};
    std::ostringstream oss{s};

    std::cout << oss.tellp() << "\n";
    oss << "Hello";
    std::cout << oss.tellp() << "\n";
}

実行結果:

0
5
#include <iostream>
#include <string>

int main()
{
    std::string s {};
    
    std::cout << std::cin.tellg() << "\n";
    std::cin >> s;
    std::cout << std::cin.tellg() << "\n";
}

実行結果:

0
5

シーク

seekgメンバ関数5seekpメンバ関数6を使用すると、読み書きを行わずに、ファイルポジションを強制的に移動できます。ファイルポジションを移動させる操作のことをシーク (seek) と呼びます。

seekgメンバ関数は読み取りのファイルポジションを、seekpメンバ関数は書き込みのファイルポジションを移動させます。

seekg の gget、seekp の pput のことです。

seekgメンバ関数と seekpメンバ関数には、それぞれ2通りの呼び出し方があります。いずれの場合も、対象になったストリームへの参照が返されます。また、シークが失敗した場合は、failメンバ関数が true を返す状態になります7

ifs.seekg(距離, 基準);
ifs.seekg(位置);

ofs.seekp(距離, 基準);
ofs.seekp(位置);

「基準」には、以下の定数から1つ選んで指定します。これらは std::ios_base::seekdir という型です。

意味
std::ios_base::beg 先頭
std::ios_base::cur 現在のファイルポジション
std::ios_base::end 末尾(※バイナリ形式では保証されない)

選んだ「基準」の位置から、「距離」の分だけ加算したところが新しいファイルポジションになります。「距離」はバイト数ですが、負数でも構いません。負数ならファイルの先頭の側に向かってファイルポジションを動かすことになります。

ifs.seekg(30, std::ios_base::beg);  // 先頭から 30バイトのところへ移動
ofs.seekp(-10, std::ios_base::cur); // 10バイトだけ先頭に向かって移動

上表にも注意書きがありますが、バイナリ形式の場合、std::ios_base::end を指定して末尾へ移動させられるかどうかは保証がありません。8

また、テキスト形式の場合に「距離」に指定できる値は、0 か、tellgメンバ関数や tellpメンバ関数で取得した正しいファイルポジションでなければなりません。ファイルポジションを指定する場合は、「基準」は std::ios_base::beg でなければなりません。8


「位置」のほうは、先頭からのバイト数を符号無し整数型で指定すると、その位置にファイルポジションが移動します。

対象がファイルの場合に「位置」に安全に指定できる値は、tellgメンバ関数や tellpメンバ関数で取得した正しいファイルポジションだけです(成功したときに返される値のみ)。9

ifs.seekg(100);    // 決め打ちされた 100 は適切な位置ではないかもしれない


次のサンプルプログラムは、シークによって、途中の 8バイトを読み飛ばしています。

#include <cstdint>
#include <fstream>
#include <iostream>

int main()
{
    std::ifstream ifs("test.bin", std::ios_base::in | std::ios_base::binary);

    // データを読み込む。途中の 8バイトを飛ばす
    std::int32_t a {};
    char c[4] {};
    ifs.read(reinterpret_cast<char*>(&a), sizeof(a));
    ifs.seekg(8, std::ios_base::cur);  // 8バイト分だけ読み飛ばす
    ifs.read(c, sizeof(c));

    std::cout << std::hex << a << "\n"
              << std::dec << c << "\n";
}

test.bin(バイナリで表示):

56 34 12 00 23 01 CD AB 00 00 00 00 78 79 7A 00

実行結果:

123456
xyz

書き込みでシークを使う場面はそう多くないかもしれませんが、既存のファイルの一部分を書き換えるとか、ファイルの末尾に書き足すといったときに使えます。ただし、std::ios_base::out を指定したオープンでは、既存のファイルの内容が消されてしまうため、以下のような方法をとらなければなりません。

  1. std::ios_base::app の指定を追加して、追記モードにする
  2. std::fstream を使い、読み書き両用にする

1番は、ファイルの末尾に書き足したい場合にしか使えませんが、そのような用途ならこれが簡単です。このあと「追記」のところで説明します。

2番は、読み込みと書き込みが両方とも行える方法です。読み込みのことも考慮されたオープンなので、ファイルの中身が消えることはありません。このあと「読み書き両用モード (std::fstream)」のところで説明します。

追記

オープンモードに std::ios_base::app を指定すると、書き込みのたびに自動的にファイルポジションが末尾に移動します。つまり、既存のファイルの中身の末尾に付け足すように書き込みを行うことになります。このような書き込み方法は追記 (append) と呼ばれます(app という名前はここから来ています)。

#include <fstream>
#include <iostream>

int main()
{
    std::ofstream ofs("test.bin", std::ios_base::out | std::ios_base::binary | std::ios_base::app);

    char a[] {"aaa"};
    ofs.write(a, sizeof(a));

    char b[] {"bbb"};
    ofs.seekp(0);             // 先頭に戻しても・・
    ofs.write(b, sizeof(b));  // 末尾に書き込まれる
}

実行結果(test.bin(バイナリで表示)):

61 61 61 00 62 62 62 00

読み書き両用モード (std::fstream)

1つのファイルに対して、読み込みと書き込みの両方を行いたいことがあります。その場合には std::fstream10 を使い、オープンモードに std::ios_base::in と std::ios_base::out の両方を指定します。std::fstream は <fstream> で定義されています。

使い方は、std::ifstream と std::ofstream を合わせたものだと思えばいいです。readメンバ関数も writeメンバ関数も備えており、seekgメンバ関数と seekpメンバ関数もそれぞれ使えます。

#include <fstream>
#include <iostream>
#include <vector>

int main()
{
    // バイナリ形式で読み書き両用オープン
    constexpr auto path = "test.bin";
    std::fstream fs(path, std::ios_base::in | std::ios_base::out | std::ios_base::binary);
    if (fs.fail()) {
        std::cerr << "File open error: " << path << "\n";
        return EXIT_FAILURE;
    }

    // 読み込み
    std::istreambuf_iterator<char> it_fs_begin(fs);
    std::istreambuf_iterator<char> it_fs_end {};
    std::vector<char> input_data(it_fs_begin, it_fs_end);
    if (fs.fail()) {
        std::cerr << "File read error: " << path << "\n";
        std::quick_exit(0);
    }

    // ファイルの先頭4バイトを上書きする
    char a[] {"aaa"};
    fs.seekp(0);
    fs.write(a, sizeof(a));

    fs.close();
    if (fs.fail()) {
        std::cerr << "File close error: " << path << "\n";
        std::quick_exit(0);
    }
}

test.bin(バイナリで表示):

56 34 12 00 23 01 CD AB 00 00 00 00 78 79 7A 00

実行結果(test.bin をバイナリで表示):

61 61 61 00 23 01 CD AB 00 00 00 00 78 79 7A 00

シークの関数が seekgメンバ関数と seekpメンバ関数に分かれているので、ファイルポジションを読み書きそれぞれ分離して制御できるように思えますが、ファイルポジションは読み書きとも共通のものが1つあるだけです。つまり、seekgメンバ関数を使っても seekpメンバ関数を使っても、実は同じ結果になります。11

文字列ストリーム(std::stringstream)の場合は、読み書きのファイルポジションを別個に制御できます12

ファイルサイズを調べる

ファイルサイズを調べるには、以下のようにいくつかの方法がありますが、C++14 の時点では、これを使っておけばいいといえるような決定的なものがありません。

  1. ファイルポジションの値を利用する
  2. stat関数など、C++ の標準ではない関数を使う
  3. 1バイトずつ読み込みながらカウントすることを、末尾に到達するまで繰り返す
  4. (C++17 から) std::filesystem::file_size関数13を使う

1番は昔からよく紹介されている方法ですが、保証のない動作を使う、環境依存の方法です。

2番は C++ の標準ライブラリの関数ではないため、環境ごとに使える方法を探す必要があります。

3番は、実際のファイルサイズによって、処理に掛かる時間が変わりますし、大きければ非常に長い時間を要します。つまり性能が極端に悪い方法です。

C++14 では2番の方法が妥当ですが、複数の環境に対応したプログラムを作るには、#if で切り分けるなどして、それぞれのコードを書かなければなりません。ここでは、保証がないことを分かったうえで、よく知られているファイルポジションを使った方法と、stat関数を使う方法を紹介します。もし C++17 が使えるなら、4番の方法を選びましょう。

ファイルポジションの値を利用する

ファイルをバイナリ形式でオープンしていれば、ファイルの末尾のファイルポジションはファイルサイズと一致すると考えられますから、これを利用するという方法です。しかし、前述しているとおり、バイナリ形式では、末尾へのシークは保証されていないため、この方法はうまくいかない可能性があります

実際に試してみます。std::seekg関数を使ってシークするのでもいいですが、std::ios_base::ate を指定すれば末尾にファイルポジションがある状態で始まるので、このほうが簡単です(この方法でも内部的にはシークしています1)。なお、オープンしたときにファイルの内容がクリアされないように、std::ifstream を使う必要があります。

#include <cassert>
#include <cstdint>
#include <fstream>
#include <iostream>

// ファイルサイズを取得する
bool get_file_size(const std::string& path, std::uintmax_t* file_size)
{
    assert(file_size);

    // ファイルポジションが末尾にある状態でオープン
    std::ifstream ifs(path, std::ios_base::in | std::ios_base::binary | std::ios_base::ate);
    if (ifs.fail()) {
        return false;
    }

    // ファイルポジションを調べる=ファイルサイズと一致する
    *file_size = ifs.tellg();
    return true;
}

int main()
{
    constexpr auto path = "test.bin";

    std::uintmax_t file_size {};
    if (get_file_size(path, &file_size)) {
        std::cout << file_size << "\n";
    }
    else {
        std::cerr << "Get file size error: " << path << "\n";
    }
}

test.bin(バイナリで表示):

56 34 12 00 23 01 CD AB 00 00 00 00 78 79 7A 00

実行結果:

16

ファイルサイズを表現する専用の型は特にありません。ファイルが非常に大きい場合も考えて、std::uintmax_t を選んでおくのが無難でしょう(「整数型」のページを参照)。

stat関数を使う

stat関数14を使う方法は、ファイルポジションによる方法よりも確実ですが、C++ の標準ライブラリ関数ではないため、処理系によっては使えない可能性があります。

stat関数は、指定したファイルに関するさまざまな情報を一括で取得します。この情報の一部として、ファイルサイズが含まれているので、これを取り出せばいいというわけです。stat関数を使うには、<sys/stat.h> をインクルードする必要があります。

#include <cassert>
#include <cstdint>
#include <iostream>
#include <sys/stat.h>

// ファイルサイズを取得する
bool get_file_size(const std::string& path, std::uintmax_t* file_size)
{
    assert(file_size);

    // ファイルの情報を取得
    struct stat st {};
    if (stat(path.c_str(), &st) != 0) {
        return false;
    }

    // ファイルかどうか
    // S_ISREG(st.st_mode); の方がシンプルだが、Visual Studio では使えない。
    if ((st.st_mode & S_IFMT) != S_IFREG) {
        return false;
    }

    *file_size = st.st_size;
    return true;
}

int main()
{
    constexpr auto path = "test.bin";

    std::uintmax_t file_size {};
    if (get_file_size(path, &file_size)) {
        std::cout << file_size << "\n";
    }
    else {
        std::cerr << "Get file size error: " << path << "\n";
    }
}

test.bin(バイナリで表示):

56 34 12 00 23 01 CD AB 00 00 00 00 78 79 7A 00

実行結果:

16

stat関数の引数は2つあり、第1引数に対象のファイルのパスを、第2引数に結果を受け取る stat構造体へのポインタを指定します。戻り値は、成功したら 0、失敗したら -1 です。

関数名と構造体名がどちらも stat であるため、変数を宣言するときには、struct stat st; のように structキーワードを明示して、構造体であることを明確にする必要があります。

stat関数は、ファイルではないもの(ディレクトリなど)の情報を取得するためにも使えるため、取得した結果が本当にファイルのものであるかどうかも確認しています。これは、stat構造体の st_modeデータメンバを見ることで分かりますが、ここにはビット単位で情報が詰め込まれているので、S_IFMTマクロとのビット単位AND を取って、S_IFREGマクロ(通常のファイルかどうかという意味の値)との比較を行います(「ビット単位の処理」のページを参照)。

ファイルかどうかを判断するために、S_ISREGマクロが使える場合があります(S_ISREG(st.st_mode))。Visual Studio 2015 では使用できません。

取得した情報がたしかにファイルのものなのであれば、st_sizeデータメンバに入っている値がファイルサイズです。

バイナリエディタのプログラムの修正

最後にバイナリエディタのプログラムの修正を行います。やることは2つです。

  1. –sizeオプションで、実際のファイルサイズを越えた指定をされたら、ファイルサイズ分までで出力を止める
  2. –beginオプションを実装する

いずれも、このページで解説した方法を使うだけなので特に解説はしません。完成版のプログラムは次のようになります(main.cpp 以外には変更はありません)。

// main.cpp
#include <cassert>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <sstream>
#include <string>
#include <vector>
#include <sys/stat.h>
#include "analysis_cmdline_args.h"

using namespace std;

namespace beditor {

    constexpr auto app_name = "binary_editor";
    constexpr auto version_str = "1.0.3";

    // オプション
    using option_t = unsigned int;
    constexpr option_t option_none {0};          // オプションなし
    constexpr option_t option_lower_case {0b1};  // 小文字で表示する
    static option_t option {option_none};

    // 読み込むサイズ
    static uintmax_t read_size {numeric_limits<uintmax_t>::max()};

    // 出力開始位置
    static uintmax_t begin_pos {0};

    static bool get_file_size(const string& path, uintmax_t* file_size);
    static vector<char> read_file(ifstream& ifs, uintmax_t begin_pos, uintmax_t raed_size);
    static void print_help();
    static void print_version();
    static void on_option_help(const string& param);
    static void on_option_version(const string& param);
    static void on_option_lower(const string& param);
    static void on_option_size(const string& param);
    static void on_option_begin(const string& param);

    // オプション一覧
    static const vector<cmd_args::OptionInfo> options {
        { "-l", "--lower", on_option_lower, false },
        {"-s", "--size", on_option_size, true},
        {"-b", "--begin", on_option_begin, true},
    };

    // 大文字・小文字
    enum class Letter {
        upper,
        lower,
    };


    // バイト列を標準出力へ出力する
    //
    // address: 対象のバイト列を指し示すポインタ
    // size: 対象のバイト列のバイト数
    // letter: 16進数のアルファベットの表示方法
    static void print_byte_string(const void* address, size_t size, Letter letter);
}

int main(int argc, char** argv)
{
    // コマンドライン引数を vector に詰め替える
    auto cmdline_args = cmd_args::argv_to_vector(argc, argv);
    
    // ヘルプとバージョン情報のオプションを登録
    cmd_args::regist_help({"-h", "--help", beditor::on_option_help});
    cmd_args::regist_version({"-v", "--version", beditor::on_option_version});

    // コマンドライン引数の解析
    switch (cmd_args::analysis_cmdline_args(cmdline_args, 2, beditor::options)) {
    case cmd_args::AnalysisCmdlineArgsResult::correct:
        break;

    case cmd_args::AnalysisCmdlineArgsResult::correct_exit:
        quick_exit(EXIT_SUCCESS);
        break;

    case cmd_args::AnalysisCmdlineArgsResult::error_args_not_enough:
        cerr << "コマンドライン引数の指定が正しくありません。\n";
        beditor::print_help();
        quick_exit(EXIT_FAILURE);
        break;

    case cmd_args::AnalysisCmdlineArgsResult::error_invalid_option:
        cerr << "不正なオプションが指定されています。\n";
        beditor::print_help();
        quick_exit(EXIT_FAILURE);
        break;

    case cmd_args::AnalysisCmdlineArgsResult::error_invalid_parameter:
        cerr << "オプションのパラメータに不備があります。\n";
        beditor::print_help();
        quick_exit(EXIT_FAILURE);
        break;

    default:
        break;
    }

    // ファイルサイズを取得
    string path {cmdline_args.at(1)};
    uintmax_t file_size {};
    if (beditor::get_file_size(path, &file_size) == false) {
        cerr << path << "のサイズ取得に失敗しました。\n";
        quick_exit(EXIT_FAILURE);
    }

    // ファイルサイズを越えないように調整する
    if (beditor::begin_pos > file_size) {
        beditor::begin_pos = file_size;
    }
    if (beditor::begin_pos + beditor::read_size > file_size) {
        beditor::read_size = file_size - beditor::begin_pos;
    }

    // 対象のファイルをオープン
    ifstream ifs(path, ios_base::in | ios_base::binary);
    if (!ifs) {
        cerr << path << "が開けません。\n";
        quick_exit(EXIT_FAILURE);
    }

    // ファイルの内容を読み込む
    vector<char> bytes {beditor::read_file(ifs, beditor::begin_pos, beditor::read_size)};

    // バイト列を出力
    beditor::print_byte_string(
        bytes.data(),
        bytes.size(),
        (beditor::option & beditor::option_lower_case) ? beditor::Letter::lower : beditor::Letter::upper
    );
}

namespace beditor {

    // ファイルサイズを返す
    static bool get_file_size(const string& path, uintmax_t* file_size)
    {
        assert(file_size);

        // ファイルの情報を取得
        struct stat st {};
        if (stat(path.c_str(), &st) != 0) {
            return false;
        }

        // ファイルかどうか
        // S_ISREG(st.st_mode); の方がシンプルだが、Visual Studio では使えない。
        if ((st.st_mode & S_IFMT) != S_IFREG) {
            return false;
        }

        *file_size = st.st_size;
        return true;
    }

    // ファイルから必要なサイズ分だけ読み込む
    static vector<char> read_file(ifstream& ifs, uintmax_t begin_pos, uintmax_t raed_size)
    {
        if (begin_pos == 0 && raed_size == numeric_limits<uintmax_t>::max()) {
            // ファイルの中身をすべて読み込む
            istreambuf_iterator<char> it_ifs_begin(ifs);
            istreambuf_iterator<char> it_ifs_end {};
            vector<char> bytes(it_ifs_begin, it_ifs_end);
            return bytes;
        }
        else {
            // ファイルの中身を必要なサイズ分だけ読み込む。
            vector<char> bytes(static_cast<vector<char>::size_type>(raed_size));
            ifs.seekg(begin_pos, ios_base::beg);
            ifs.read(bytes.data(), raed_size);
            return bytes;
        }
    }

    // ヘルプメッセージを出力
    static void print_help()
    {
        cout << "使い方:\n"
             << "> " << app_name << " 対象のファイルパス\n"
             << "> " << app_name << " 対象のファイルパス オプション\n"
             << "> " << app_name << " -h(または --help)\n"
             << "> " << app_name << " -v(または --version)\n"
             << "\n"
             << "-h(--help)    使い方を表示します。\n"
             << "-v(--version) バージョン情報を表示します。\n"
             << "\n"
             << "オプション:\n"
             << "-l (--lower)      --> 16進数のアルファベット部分を小文字で表示します。\n"
             << "-s (--size) 整数  --> 指定サイズまでで表示を終了します。"
             << "-b (--begin) 整数 --> 指定位置から表示を開始します。"
             << endl;
    }

    // バージョン情報を出力
    static void print_version()
    {
        cout << app_name << " version: " << version_str << endl;
    }

    // ヘルプオプションを発見したときに呼び出される関数
    static void on_option_help(const std::string& param)
    {
        (void)param;
        print_help();
    }

    // バージョン情報オプションを発見したときに呼び出される関数
    static void on_option_version(const std::string& param)
    {
        (void)param;
        print_version();
    }

    // 小文字化オプションを発見したときに呼び出される関数
    static void on_option_lower(const std::string& param)
    {
        (void)param;
        option |= option_lower_case;
    }

    // サイズ指定オプションを発見したときに呼び出される関数
    static void on_option_size(const string& param)
    {
        istringstream iss {param};
        uintmax_t size {};
        iss >> size;
        if (iss) {
            read_size = size;
        }
        else {
            cerr << "サイズの指定が不適切です。\n";
            quick_exit(EXIT_FAILURE);
        }
    }

    // 出力開始位置の指定オプションを発見したときに呼び出される関数
    static void on_option_begin(const string& param)
    {
        istringstream iss {param};
        uintmax_t begin {};
        iss >> begin;
        if (iss) {
            begin_pos = begin;
        }
        else {
            cerr << "出力開始位置の指定が不適切です。\n";
            quick_exit(EXIT_FAILURE);
        }
    }

    // バイト列を標準出力へ出力する
    static void print_byte_string(const void* address, size_t size, Letter letter)
    {
        auto p = static_cast<const unsigned char*>(address);
        auto remain_byte = size;

        while (remain_byte > 0) {
            size_t byte_num {remain_byte >= 16 ? 16 : remain_byte};

            for (size_t i {0}; i < byte_num; ++i) {
                cout << setw(2) << setfill('0') << hex
                     << (letter == Letter::upper ? uppercase : nouppercase)
                     << static_cast<unsigned int>(*p) << " ";
                ++p;
            }
            cout << "\n";

            remain_byte -= byte_num;
        }
    }
}
// analysis_cmdline_args.cpp
#include "analysis_cmdline_args.h"
#include <algorithm>
#include <cassert>
#include <iterator>

using namespace std;

namespace cmd_args {

    static OptionInfo help_info {};
    static OptionInfo version_info {};


    // コマンドライン引数を std::vector<std::string> に格納して返す
    vector<string> argv_to_vector(int argc, char** argv)
    {
        vector<string> vec {};
        for (int i {0}; i < argc; ++i) {
            vec.push_back(argv[i]);
        }
        return vec;
    }

    // ヘルプオプションを登録する
    void regist_help(const OptionInfo& info)
    {
        assert(info.short_name != "" || info.long_name != "");
        assert(info.callback_func);

        help_info = info;
    }

    // バージョン情報オプションを登録する
    void regist_version(const OptionInfo& info)
    {
        assert(info.short_name != "" || info.long_name != "");
        assert(info.callback_func);

        version_info = info;
    }

    // コマンドライン引数を解析する
    AnalysisCmdlineArgsResult analysis_cmdline_args(const vector<string>& cmdline_args, std::size_t option_start_index, const vector<OptionInfo>& options)
    {
        // コマンドライン引数の個数チェック
        if (cmdline_args.size() < 2) {
            return AnalysisCmdlineArgsResult::error_args_not_enough;
        }

        // ヘルプ
        if (cmdline_args.at(1) == help_info.short_name || cmdline_args.at(1) == help_info.long_name) {
            help_info.callback_func("");
            return AnalysisCmdlineArgsResult::correct_exit;
        }

        // バージョン情報
        if (cmdline_args.at(1) == version_info.short_name || cmdline_args.at(1) == version_info.long_name) {
            version_info.callback_func("");
            return AnalysisCmdlineArgsResult::correct_exit;
        }

        // オプションを探す
        for (size_t i {option_start_index}; i < cmdline_args.size(); ++i) {
            auto it_opt = find_if(
                cbegin(options),
                cend(options),
                [arg = cmdline_args.at(i)](const OptionInfo& opt) {
                    return arg == opt.short_name || arg == opt.long_name;
                }
            );

            if (it_opt == cend(options)) {
                // 該当するオプションがない
                return AnalysisCmdlineArgsResult::error_invalid_option;
            }
            else {
                // 発見したオプションに応じた登録関数をコールバック
                if (it_opt->has_param) {
                    ++i;
                    if (i < cmdline_args.size()) {
                        it_opt->callback_func(cmdline_args.at(i));
                    }
                    else {
                        return AnalysisCmdlineArgsResult::error_invalid_parameter;
                    }
                }
                else {
                    it_opt->callback_func("");
                }
            }
        }

        return AnalysisCmdlineArgsResult::correct;
    }
}
// analysis_cmdline_args.h
#ifndef ANALYSIS_CMDLINE_ARGS_INCLUDED
#define ANALYSIS_CMDLINE_ARGS_INCLUDED

#include <functional>
#include <string>
#include <vector>

namespace cmd_args {

    // コマンドライン解析の結果
    enum class AnalysisCmdlineArgsResult {
        correct,                    // 正しい指定
        correct_exit,               // 正しい指定だが、プログラムは終了させる
        error_args_not_enough,      // エラー。コマンドライン引数が不足
        error_invalid_option,       // エラー。不正なオプションを発見
        error_invalid_parameter,    // エラー。パラメータの指定が正しくない
    };

    // オプション発見時のコールバック関数型
    using option_callback_t = std::function<void(const std::string&)>;

    // オプション情報
    struct OptionInfo {
        std::string                 short_name;     // 短い名前(不要なら空文字列)
        std::string                 long_name;      // 長い名前(不要なら空文字列)
        option_callback_t           callback_func;  // オプション発見時にコールバックする関数
        bool                        has_param;      // パラメータの有無
    };



    // コマンドライン引数を std::vector<std::string> に格納して返す
    std::vector<std::string> argv_to_vector(int argc, char** argv);

    // ヘルプオプションを登録する
    void regist_help(const OptionInfo& info);

    // バージョン情報オプションを登録する
    void regist_version(const OptionInfo& info);

    // コマンドライン引数を解析する
    AnalysisCmdlineArgsResult analysis_cmdline_args(const std::vector<std::string>& cmdline_args, std::size_t option_start_index, const std::vector<OptionInfo>& options);

}

#endif

以上でバイナリエディタのプログラムは完成としておきます。次のページからは新しいテーマに移ります。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (基本★)

std::istringstream に対してもシーク操作ができることを、プログラムを書いて確認してください。

解答・解説

問題2 (基本★★)

std::stringstream では、読み書きそれぞれのファイルポジションを制御できます。プログラムを書いて確認してください。

解答・解説

問題3 (応用★★)

ファイルサイズを取得する方法として、本編で解説しなかった、「1バイトずつ読み込みながらカウントすることを、末尾に到達するまで繰り返す」プログラムを作成してください。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る