関数ポインタとラムダ式 解答ページ | Programming Place Plus 新C++編

トップページ新C++編関数ポインタとラムダ式

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (基本★★)

int型の配列から、指定の条件を満たす最初の要素を見つけて、その要素を指し示すポインタを返す関数を作成してください。見つからない場合はヌルポインタを返すようにしてください。


「指定の条件」とあるので、条件が指定できなければなりません。そのためには、引数に、条件を記述した関数を渡してやります。実際には、C++ で関数に関数を渡すことはできませんが(本編解説)、関数ポインタ(本編解説)やラムダ式(本編解説)を使えば目的を達成できます。

関数ポインタを使う場合は、次のように実装できます。ここでは条件は「奇数であること」とします。

#include <cassert>
#include <iostream>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

using find_condition_t = bool (*)(int); // 探索条件を記述する関数ポインタ型

// 条件を満たす最初の要素を探す
int* find(int* array, std::size_t size, find_condition_t cond)
{
    assert(array);
    assert(cond);

    for (std::size_t i {0}; i < size; ++i) {
        if (cond(array[i])) {
            return &array[i];
        }
    }
    return nullptr;
}

// 奇数かどうか判定する
bool is_odd(int value)
{
    return value & 0b1;
}

int main()
{
    int array[] {4, 6, 7, 8, 11};

    int* p {find(array, SIZE_OF_ARRAY(array), is_odd)};
    if (p) {
        std::cout << *p << "\n";
    }
    else {
        std::cout << "not found.\n";
    }
}

実行結果:

7

関数ポインタ型の記述は複雑で分かりづらいので、using や typedef を使って別名を定義するのはよく使われる方法です。ここでは、using find_condition_t = bool (*)(int) なので、int型の仮引数をもち、bool型の戻り値を返す関数を指し示す関数ポインタ型に find_condition_t という別名をつけています。find_condition_t型に合うように、int型の仮引数をもち、bool型の戻り値を返す is_odd関数を定義して、find関数に渡せば、is_odd関数に記述した条件を使った探索が行えます。


find関数の実装を一切変えずに、条件をラムダ式で指定することができます。

#include <cassert>
#include <iostream>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

using find_condition_t = bool (*)(int); // 探索条件を記述する関数ポインタ型

// 条件を満たす最初の要素を探す
int* find(int* array, std::size_t size, find_condition_t cond)
{
    assert(array);
    assert(cond);

    for (std::size_t i {0}; i < size; ++i) {
        if (cond(array[i])) {
            return &array[i];
        }
    }
    return nullptr;
}

int main()
{
    int array[] {4, 6, 7, 8, 11};

    int* p {find(array, SIZE_OF_ARRAY(array), [](int e) -> bool { return e & 0b1; } )};
    if (p) {
        std::cout << *p << "\n";
    }
    else {
        std::cout << "not found.\n";
    }
}

実行結果:

7

関数ポインタ型である find関数の引数にラムダ式を記述できるのは、ラムダ式から関数ポインタへの暗黙的な型変換が可能であるためです(本編解説)(ただし、キャプチャ機能を使っている場合は除きます)。条件を表現するコードがごく短いなら、いちいち関数を定義するより、ラムダ式で記述したほうが簡単ですし、その場に条件式が記述できるので分かりやすくもあります。

ラムダ式の本体が返す戻り値の型は、ほとんどの場合、型推論に任せてしまえばいいですが、このプログラムでは明示的に bool型を指定しています。これは、e & 0b1 という式の結果は int型なので、型推論に任せると、find_condition_t の戻り値型と一致しないためです。


なお、この問題の find関数がしていることは、std::find_if関数(「要素を探索する」のページを参照)がすでに実現しています。実戦では std::find_if関数を使いましょう。

問題2 (基本★★)

int型の配列から、条件を満たす要素を順番に探し、そのつどコールバック関数に渡す関数を作成してください。


考え方は問題1と変わりませんが、今回は要素を見つけるたびにコールバックされる関数を指定しなければならないので、2つの関数ポインタかラムダ式を渡すようにします。ここではラムダ式を使ってやってみます。

#include <cassert>
#include <iostream>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

using find_condition_t = bool (*)(int);   // 探索条件を記述する関数ポインタ型
using found_callback_t = void (*)(int);   // 要素を見つけたときにコールバックする関数の型

// 条件を満たすすべての要素を探して、見つけるたびにコールバックする
void find_in_order(int* array, std::size_t size, find_condition_t cond, found_callback_t on_found)
{
    assert(array);
    assert(cond);
    assert(on_found);

    for (std::size_t i {0}; i < size; ++i) {
        if (cond(array[i])) {
            on_found(array[i]);
        }
    }
}

int main()
{
    int array[] {4, 6, 7, 8, 11};

    find_in_order(array, SIZE_OF_ARRAY(array),
        [](int e) -> bool { return e & 0b1; },
        [](int e) { std::cout << e << "\n"; }
    );
}

実行結果:

7
11

条件は問題1と同じ「奇数であること」です。それに加えて、条件を満たす要素を発見するたびにコールバックされる関数をラムダ式で記述しています。

問題3 (基本★★)

問題2で作成した関数に与える条件として、「標準入力から入力された2つの整数min、max の範囲内の値であること」を指定したプログラムを作成してください。


標準入力から入力された値を使うため、いったん変数に受け取る必要があります。

int main()
{
    int min {};
    int max {};
    std::cin >> min >> max;
}

この変数を使ってラムダ式を記述すると困ったことになります。

int main()
{
    int min {};
    int max {};
    std::cin >> min >> max;

    int array[] {4, 6, 7, 8, 11};

    find_in_order(array, SIZE_OF_ARRAY(array),
        [](int e) -> bool { return min <= e && e <= max; },  // コンパイルエラー
        [](int e) { std::cout << e << "\n"; }
    );
}

ラムダ式の本体で変数min、max を使おうとしていますが、ラムダ式の本体では、(静的ローカル変数を除いて)ローカル変数を使用することができません。ローカル変数(や仮引数)を使いたいときには、キャプチャを行う必要があります(本編解説)。

しかし、キャプチャを行ったとしても、まだコンパイルエラーになります。

int main()
{
    int min {};
    int max {};
    std::cin >> min >> max;

    int array[] {4, 6, 7, 8, 11};

    // コンパイルエラー
    find_in_order(array, SIZE_OF_ARRAY(array),
        [min, max](int e) -> bool { return min <= e && e <= max; },
        [](int e) { std::cout << e << "\n"; }
    );
}

今度の問題は、find_in_order関数の第3引数が関数ポインタ型であることです。キャプチャを使っているラムダ式は、関数ポインタに変換することができません(本編解説)。

そこで今度は、find_in_order関数の型をなおします。std::function を使うと、関数ポインタでも、キャプチャ付きのラムダ式でも取り扱えるようになります(本編解説)。

#include <cassert>
#include <functional>
#include <iostream>

#define SIZE_OF_ARRAY(array)  (sizeof(array) / sizeof(array[0]))

using find_condition_t = std::function<bool(int)>;   // 探索条件を記述する関数の型
using found_callback_t = std::function<void(int)>;   // 要素を見つけたときにコールバックする関数の型

// 条件を満たすすべての要素を探して、見つけるたびにコールバックする
void find_in_order(int* array, std::size_t size, find_condition_t cond, found_callback_t on_found)
{
    assert(array);
    assert(cond);
    assert(on_found);

    for (std::size_t i {0}; i < size; ++i) {
        if (cond(array[i])) {
            on_found(array[i]);
        }
    }
}

int main()
{
    std::cout << "Please enter the min and max values。\n";
    int min {};
    int max {};
    std::cin >> min >> max;

    int array[] {4, 6, 7, 8, 11};

    find_in_order(array, SIZE_OF_ARRAY(array),
        [min, max](int e) { return min <= e && e <= max; },
        [](int e) { std::cout << e << "\n"; }
    );
}

実行結果:

Please enter the min and max values。
5 10  <-- 入力された内容
6
7
8

問題4 (応用★★★)

コールバック関数を利用して、バイナリエディタのプログラムのコマンドライン引数を解析する部分を汎用的に使えるように改造してください。

最新のバイナリエディタのプログラムは、「スコープと名前空間」のページの練習問題にあります。


本編で最後に解説した std::function を使ってみることにします。次の型定義を、analysis_cmdline_args.h に置きます。

namespace cmd_args {

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

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

main.cpp に、利用できるオプションの一覧表を定義します。

namespace beditor {

    // オプション一覧
    static const vector<cmd_args::OptionInfo> options {
        { "-l", "--lower", on_option_lower },
    };
}

それぞれのコールバック関数の定義は、一時的に analysis_cmdline_args.cpp に移し替えていた関数に多少の変更を加えて、main.cpp へ戻します。合わせて、いくつかの constexpr変数などの定義も移動させます。

namespace beditor {

    // オプション
    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 void on_option_lower()
    {
        option = option_lower_case;
    }
}

次に、analysis_cmdline_args関数を修正します。まず、オプションの一覧表を渡すようにします。

// analysis_cmdline_args.h
namespace cmd_args {

    // コマンドライン引数を解析する
    AnalysisCmdlineArgsResult analysis_cmdline_args(const std::vector<std::string>& cmdline_args, const std::vector<OptionInfo>& options);
}
// main.cpp
switch (cmd_args::analysis_cmdline_args(cmdline_args, beditor::options)) {
    // ...
}

analysis_cmdline_args関数の本体を修正します。cmdline_args の中から、options に含まれているオプションと一致するものを探し、見つかり次第、コールバック関数を呼び出します。

namespace cmd_args {

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

        // オプションを探す
        for (size_t i {2}; 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 {
                // 発見したオプションに応じた登録関数をコールバック
                it_opt->callback_func();
            }
        }

        return AnalysisCmdlineArgsResult::correct;
    }
}

オプションを探す部分では std::find_if関数を使い、本編で解説したラムダ式やキャプチャ機能も活用しています。

ここで、コマンドライン引数の個数に関する問題があります。argv[0] にプログラムの名前が入ってくるため、cmdline_args のサイズは最低でも 1 ですが、バイナリエディタのプログラムの場合は、対象のファイルの指定も必ず必要ですから、実際の最低値は 2 ということになります。オプションの指定が始まる位置は cmdline_args[2] からです。この 2 というリテラルが埋め込まれてしまっていますが、この関数は汎用的に作りたいものなので、これはよくないです。そこで、オプションが開始される位置も渡せるようにします。

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

        // オプションを探す
        for (size_t i {option_start_index}; i < cmdline_args.size(); ++i) {

        // ...
    }
}

ここまでの変更で –lowerオプションは使用できるようになりましたが、–help と –version の存在を無視しています。この2つは少し特例的な扱いが必要です。つまり、binary_editor test.bin --lower のように指定する –lowerオプションと違って、binary_editor --help とか binary_editor --version のように指定するものであり、対象ファイルの指定がありません。また、–help や –version は、情報を出力するという仕事を行ったあと、プログラムの実行は終わるのが普通です。引き続き、ほかのオプションを探すということはしません。

さきほどのコードで、「オプションを探す」という for文の中で何とかしようとすると相当面倒なことになります。この for文より先に片づけてしまったほうが簡単でしょう。事前にヘルプとバージョン情報に関する情報を渡しておく方法が考えられます。

// analysis_cmdline_args.h
namespace cmd_args {
    // ヘルプオプションを登録する
    void regist_help(const OptionInfo& info);

    // バージョン情報オプションを登録する
    void regist_version(const OptionInfo& info);
}
// analysis_cmdline_args.cpp
namespace cmd_args {

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

    // ヘルプオプションを登録する
    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) {
            // ...
    }
}
// main.cpp

int main(int argc, char** argv)
{
    // ...

    // ヘルプとバージョン情報のオプションを登録
    cmd_args::regist_help({"-h", "--help", beditor::on_option_help});
    cmd_args::regist_version({"-v", "--version", beditor::on_option_version});

    // ...
}

namespace beditor {

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

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

これで、ヘルプとバージョン情報にも対応できました。バイナリエディタに特化されたコードは analysis_cmdline_args.h/.cpp から main.cpp に追い出されています。プログラム全体は次のようになりました。

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

using namespace std;

namespace beditor {

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

    // オプション
    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 void print_help();
    static void print_version();
    static void on_option_help();
    static void on_option_version();
    static void on_option_lower();

    // オプション一覧
    static const vector<cmd_args::OptionInfo> options {
        { "-l", "--lower", on_option_lower },
    };

    // 大文字・小文字
    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;

    default:
        break;
    }

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

    // ファイルの中身をすべて読み込む
    istreambuf_iterator<char> it_ifs_begin(ifs);
    istreambuf_iterator<char> it_ifs_end {};
    vector<char> bytes(it_ifs_begin, it_ifs_end);

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

namespace beditor {

    // ヘルプメッセージを出力
    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進数のアルファベット部分を小文字で表示します。"
             << endl;
    }

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

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

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

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

    // バイト列を標準出力へ出力する
    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 {
                // 発見したオプションに応じた登録関数をコールバック
                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,       // エラー。不正なオプションを発見
    };

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

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



    // コマンドライン引数を 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

問題5 (応用★★★)

問題4のバイナリエディタのプログラムをさらに改造し、パラメータ付きオプションに対応できるようにしてください。たとえば、“–size 100” のように指定すると、100バイト分の情報だけを出力するというオプションを実装してください。

(現状、実際のファイルのサイズを越える指定がなされた場合を対処できないので、今のところ無視して構いません)。


たとえば、コールバック関数に引数を追加して、パラメータを渡すようにすることが考えられます。例では “–size 100” のように、整数のパラメータですが、汎用性を考えると、文字列として取り扱えるといいかもしれません。

namespace cmd_args {

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

analysis_cmdline_args関数の中でオプションを発見したとき、そのオプションにパラメータがあるのなら、1つ後ろにある要素を読み進めなければなりませんから、オプションにパラメータがあるかどうかという情報が必要です。

namespace cmd_args {

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

analysis_cmdline_args関数を修正します。

namespace cmd_args {

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

        // オプションを探す
        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;
    }
}

パラメータを渡すために i をインクリメントした結果、配列の末尾を越えてしまう可能性に気を付けてください。これはつまりユーザーの指定がおかしいということですが、だからといって未定義動作になってしまってはいけません。きちんとエラーメッセージを出せるようにしましょう。

int main(int argc, char** argv)

    // ...

    // コマンドライン引数の解析
    switch (cmd_args::analysis_cmdline_args(cmdline_args, 2, beditor::options)) {
    case cmd_args::AnalysisCmdlineArgsResult::error_invalid_parameter:
        cerr << "オプションのパラメータに不備があります。\n";
        beditor::print_help();
        quick_exit(EXIT_FAILURE);
        break;

        // ...
    }

    // ...
}

–sizeオプションは次のように実現できます。まず、必要な定義を追加します。

namespace beditor {
    static std::size_t read_size {numeric_limits<size_t>::max()};

    // 読み込むサイズ
    static void on_option_size(const string& param);

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

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

パラメータは std::string で渡されてくるので、std::istringstream を使って整数に変換します。成功したら、変数read_size に代入しておきます。失敗した場合は、エラーメッセージを出してプログラムを終了させます。

コマンドライン引数の解析後、対象のファイルから指定バイト数だけ読み込むように修正します。変数read_size が初期値のままの場合は、ファイルの内容をすべて読み込むことにすれば、–sizeオプションが指定されなかった場合に対応できます。

int main(int argc, char** argv)
{
    // ...

    // 対象のファイルをオープン
    string path {cmdline_args.at(1)};
    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::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 vector<char> read_file(ifstream& ifs, size_t size)
    {
        if (size == numeric_limits<size_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(size);
            ifs.read(bytes.data(), size);
            return bytes;
        }
    }
}

プログラム全体は次のようになりました。

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

using namespace std;

namespace beditor {

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

    // オプション
    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 std::size_t read_size {numeric_limits<size_t>::max()};

    static vector<char> read_file(ifstream& ifs, size_t 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 const vector<cmd_args::OptionInfo> options {
        { "-l", "--lower", on_option_lower, false },
        {"-s", "--size", on_option_size, 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)};
    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::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 vector<char> read_file(ifstream& ifs, size_t size)
    {
        if (size == numeric_limits<size_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(size);
            ifs.read(bytes.data(), 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) 整数  --> 指定サイズまでで表示を終了します。"
             << 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};
        size_t size {};
        iss >> size;
        if (iss) {
            read_size = size;
        }
        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

実のところ、–sizeオプションの実装にはまだ問題があります。実際のファイルのサイズを越える大きさを指定されると、越えた部分に 0 があるかのような出力になってしまいます。これは、read_file関数の中で、vector<char> bytes(size); のように初期化しているため、size の大きさ分の要素が確保され、char型のデフォルト値である 0 で埋められた結果です。

実際のファイルのサイズを越える指定は拒否するか、実際のサイズに合わせるような処理が必要ですが、現状、ファイルサイズを調べる方法がありません。この問題を次のページで解決します。


参考リンク



更新履歴




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