文字列操作 | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、std::string を使った文字列に対する文字列処理を取り上げます。std::string 自体はここまでのページでも長いあいだ使ってきましたが、std::vector と同じように使える機能が中心になっていたので、ここで文字列処理に特化した機能を説明します。具体的には、部分文字列の切り出し、検索、置換、大小関係の比較、算術型との相互変換です。

このページの解説は C++14 をベースとしています

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



文字列操作 🔗

前のページの練習問題で、現在のテーマプログラムであるペイントスクリプトの機能の大半が揃いました。残っている問題は少々細かい話ですが、「コマンドとパラメータがスペースで区切られた1つの文字列として入力されるので、スペースごとに分割して std::vector<std::string> のかたちにしたい」という点だけです。次のコードの中に登場する split関数を実装することが目標です。

int main()
{
    paint_script::Canvas canvas {};
    paint_script::CommandExecutor executor {canvas};

    while (true) {
        std::string input_string {};
        std::getline(std::cin, input_string);
        
        // 入力内容を空白文字ごとに分割して、std::vector に格納する
        const auto command_vec = split(input_string, " ");
        if (command_vec.empty()) {
            continue;
        }
        
        // 該当するコマンドを探して実行
        if (executor.exec(command_vec) == paint_script::CommandExecutor::ExecResult::exit_program) {
            break;
        }
    }
}

このページではこれ以外にも、std::string を使った文字列操作に関する処理をまとめて解説していきます。

部分文字列 🔗

substrメンバ関数を使うと、std::string の一部分を抜き出した新しい文字列(部分文字列)を取得できます。対象の std::string は変化しません。

substrメンバ関数は以下のように宣言されています[1](以下の宣言は説明のためのものであり、正確に標準規格と同じではありません)。

string substr(size_type pos = 0, size_type n = npos) const;

size_type は、std::string の型メンバとして定義されている型で、文字列の長さや位置を表現するために使われる整数型です。lengthメンバ関数の戻り値などで使われている型と同じものです(「符号無し整数」のページを参照)。

仮引数pos が抜き出す開始位置、n が抜き出す文字数です。n を std::string::npos にすると、末尾までを抜き出します。pos が実際に存在する文字数を超えてはいけません。n が大きい場合は末尾の文字までで止められるので問題ありません。

戻り値として部分文字列が返されます。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::string s {"abcdefghi"};

    std::cout << s.substr(2) << "\n"
              << s.substr(2, 3) << "\n"
              << s.substr(2, 100) << "\n";
}

実行結果:

cdefghi
cde
cdefghi

検索 🔗

std::string には、メンバ関数として宣言されている検索関数がいくつか存在します。

std::string でも std::vector でもそうですが、メンバ関数として用意されている関数は、文字列や配列に特化した作りになっており、<algorithm> などにある汎用的な関数よりも性能や機能性で優れていることが多いです。特に同じ名前の関数がある場合は、メンバ関数の方を優先して使ったほうが良いです。たとえば、std::string から特定の文字を探すことは、std::find関数を使っても可能です(「要素を探索する」のページを参照)が、この関数では文字列を探すことはできません。メンバ関数版の find関数ならば文字列も探せます。

文字や文字列が最初に現れる位置を探す 🔗

std::string から特定の文字列を探すには、findメンバ関数を使います。findメンバ関数は文字列の先頭から検索を行い、該当するものを発見した時点で終了します。

findメンバ関数は以下のようにオーバーロードされています[2](以下の宣言は説明のためのものであり、正確に標準規格と同じではありません)。

size_type find(const std::string& str, size_type pos = 0) const;
size_type find(const char* s, size_type pos, size_type n) const;
size_type find(const char* s, size_type pos = 0) const;
size_type find(char c, size_type pos = 0) const;

こういうものはすべての宣言を覚えておく必要はありません。全部をひっくるめて「std::string から文字や文字列を探すメンバ関数たち」と理解すれば十分です。詳細は必要なときに調べましょう。

仮引数str、s、c が探したい文字列または文字です。pos は検索を開始する位置です。n は検索する文字列の長さで、s の実際の長さより短く指定できます。なお、空文字列を探そうとすると、先頭で一致したことになります

戻り値は、検索した文字列や文字が発見できた場合はその位置(先頭から何文字目か)を返し、発見できなかった場合は std::string::npos という値を返します(std::string に静的データメンバとして定義された定数です)。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::string s1 {"abcxyzxyzabc"};
    std::string s2 {"xyz"};

    std::cout << s1.find(s2) << "\n"
              << s1.find(s2, 4) << "\n"
              << s1.find("xyz", 4) << "\n"
              << s1.find("xyzXYZ", 1, 3) << "\n"
              << s1.find('c') << "\n"
              << s1.find('c', 4) << "\n"
              << s1.find('!') << "\n";
}

実行結果:

3
6
6
3
2
11
4294967295

単に、ある文字や文字列が含まれているかどうかを知りたいときにも、findメンバ関数が利用できます。

#include <iostream>
#include <string>

int main()
{
    std::string s {"abcxyzxyzabc"};

    if (s.find("xyz") != std::string::npos) {
        std::cout << "contains.\n";
    }
    else {
        std::cout << "not contains.\n";
    }
}

実行結果:

contains.

【C++23】よりシンプルな方法として、containsメンバ関数が追加されています[3]


findメンバ関数と substrメンバ関数を使って、ペイントスクリプトで必要になっている「スペースで区切られた1つの文字列をスペースごとに分割して std::vector<std::string> のかたちに変換する」split関数を実装できます。

#include <iostream>
#include <string>
#include <vector>

namespace str_util {

    // 文字列を分割する
    //
    // str:   対象の文字列
    // delim: 区切り文字列
    // 戻り値: str を delim で区切った部分文字列を格納した vector
    std::vector<std::string> split(const std::string& str, const std::string& delim)
    {
        const auto delimLen = delim.size();
        if (delimLen == 0) {
            return {str};
        }

        std::string::size_type current {0};
        std::string::size_type found {};
        std::vector<std::string> result {};

        // 区切り文字を探しながら、その位置の手前までの文字列を追加することを繰り返す
        while ((found = str.find(delim, current)) != std::string::npos) {
            result.push_back(str.substr(current, found - current));
            current = found + delimLen;
        }

        // 残った部分を追加
        result.push_back(str.substr(current, str.size() - current));

        return result;
    }

}

int main()
{
    std::string input_string {"fill 0 255 127"};
    const auto command_vec = str_util::split(input_string, " ");
    for (auto& s : command_vec) {
        std::cout << s << "\n";
    }
}

実行結果:

fill
0
255
127

文字や文字列が最後に現れる位置を探す 🔗

rfindメンバ関数を使うと、文字や文字列が最後に現れる位置を探せます。

rfindメンバ関数は以下のようにオーバーロードされています[4](以下の宣言は説明のためのものであり、正確に標準規格と同じではありません)。

size_type rfind(const std::string& str, size_type pos = 0) const;
size_type rfind(const char* s, size_type pos, size_type n) const;
size_type rfind(const char* s, size_type pos = 0) const;
size_type rfind(char c, size_type pos = 0) const;

引数と戻り値の意味や使い方は findメンバ関数と同じです。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::string s1 {"abcxyzxyzabc"};
    std::string s2 {"xyz"};

    std::cout << s1.rfind(s2) << "\n"
              << s1.rfind(s2, 5) << "\n"
              << s1.rfind("xyz", 5) << "\n"
              << s1.rfind("xyzXYZ", 8, 3) << "\n"
              << s1.rfind('c') << "\n"
              << s1.rfind('c', 5) << "\n"
              << s1.rfind('!') << "\n";
}

実行結果:

6
3
3
6
11
2
4294967295

いずれかの文字が最初に現れる位置を探す 🔗

find_first_ofメンバ関数は、指定の文字や文字列に含まれているいずれかの文字が最初に現れる位置を探します。

find_first_ofメンバ関数は以下のようにオーバーロードされています[5](以下の宣言は説明のためのものであり、正確に標準規格と同じではありません)。

size_type find_first_of(const std::string& str, size_type pos = 0) const;
size_type find_first_of(const char* s, size_type pos, size_type n) const;
size_type find_first_of(const char* s, size_type pos = 0) const;
size_type find_first_of(char c, size_type pos = 0) const;

引数や戻り値の意味は findメンバ関数と同じです。違いは、仮引数str、s に指定した文字列との完全な一致ではなく、そこに含まれている文字のいずれかが見つけるという点です。仮引数c を使うタイプは1文字しか指定しないので、実質 findメンバ関数と同じです。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::string s1 {"abcxyzxyzabc"};
    std::string s2 {"za"};

    std::cout << s1.find_first_of(s2) << "\n"
              << s1.find_first_of(s2, 4) << "\n"
              << s1.find_first_of("xyz", 4) << "\n"
              << s1.find_first_of("xyzXYZ", 1, 3) << "\n"
              << s1.find_first_of('c') << "\n"
              << s1.find_first_of('c', 4) << "\n"
              << s1.find_first_of('!') << "\n";
}

実行結果:

0
5
4
3
2
11
4294967295

いずれかの文字が最後に現れる位置を探す 🔗

find_last_ofメンバ関数は、指定の文字や文字列に含まれているいずれかの文字が最後に現れる位置を探します。

find_last_ofメンバ関数は以下のようにオーバーロードされています[6](以下の宣言は説明のためのものであり、正確に標準規格と同じではありません)。

size_type find_last_of(const std::string& str, size_type pos = 0) const;
size_type find_last_of(const char* s, size_type pos, size_type n) const;
size_type find_last_of(const char* s, size_type pos = 0) const;
size_type find_last_of(char c, size_type pos = 0) const;

引数や戻り値の意味は findメンバ関数と同じです。find_first_ofメンバ関数との違いは、最後に見つけた位置を返すという点だけです。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::string s1 {"abcxyzxyzabc"};
    std::string s2 {"za"};

    std::cout << s1.find_last_of(s2) << "\n"
              << s1.find_last_of(s2, 5) << "\n"
              << s1.find_last_of("xyz", 5) << "\n"
              << s1.find_last_of("xyzXYZ", 8, 3) << "\n"
              << s1.find_last_of('c') << "\n"
              << s1.find_last_of('c', 5) << "\n"
              << s1.find_last_of('!') << "\n";
}

実行結果:

9
5
5
8
11
2
4294967295

いずれの文字も現れない最初の位置を探す 🔗

find_first_not_ofメンバ関数は、指定の文字や文字列に含まれているいずれの文字でもない文字が現れる最初の位置を返します。

find_first_not_ofメンバ関数は以下のようにオーバーロードされています[7](以下の宣言は説明のためのものであり、正確に標準規格と同じではありません)。

size_type find_first_not_of(const std::string& str, size_type pos = 0) const;
size_type find_first_not_of(const char* s, size_type pos, size_type n) const;
size_type find_first_not_of(const char* s, size_type pos = 0) const;
size_type find_first_not_of(char c, size_type pos = 0) const;

引数や戻り値の意味は findメンバ関数と同じです。find_first_ofメンバ関数との違いは、含まれている文字のいずれでもない文字の出現を探すという点だけです。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::string s1 {"abcxyzxyzabc"};
    std::string s2 {"za"};

    std::cout << s1.find_first_not_of(s2) << "\n"
              << s1.find_first_not_of(s2, 4) << "\n"
              << s1.find_first_not_of("xyz", 4) << "\n"
              << s1.find_first_not_of("xyzXYZ", 1, 3) << "\n"
              << s1.find_first_not_of('c') << "\n"
              << s1.find_first_not_of('c', 4) << "\n"
              << s1.find_first_not_of('!') << "\n";
}

実行結果:

1
4
9
1
0
4
0

いずれの文字も現れない最後の位置を探す 🔗

find_last_not_ofメンバ関数は、指定の文字や文字列に含まれているいずれの文字でもない文字が現れる最後の位置を返します。

find_last_not_ofメンバ関数は以下のようにオーバーロードされています[8](以下の宣言は説明のためのものであり、正確に標準規格と同じではありません)。

size_type find_last_not_of(const std::string& str, size_type pos = 0) const;
size_type find_last_not_of(const char* s, size_type pos, size_type n) const;
size_type find_last_not_of(const char* s, size_type pos = 0) const;
size_type find_last_not_of(char c, size_type pos = 0) const;

引数や戻り値の意味は findメンバ関数と同じです。find_last_ofメンバ関数との違いは、含まれている文字のいずれでもない文字の出現を探すという点だけです。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::string s1 {"abcxyzxyzabc"};
    std::string s2 {"za"};

    std::cout << s1.find_last_not_of(s2) << "\n"
              << s1.find_last_not_of(s2, 5) << "\n"
              << s1.find_last_not_of("xyz", 5) << "\n"
              << s1.find_last_not_of("xyzXYZ", 8, 3) << "\n"
              << s1.find_last_not_of('c') << "\n"
              << s1.find_last_not_of('c', 5) << "\n"
              << s1.find_last_not_of('!') << "\n";
}

実行結果:

11
4
2
2
10
5
11

置換 🔗

replaceメンバ関数を使うと、std::string の内容の一部を別の文字列に置き換えられます。ただしこの関数は「ある指定した文字列を探して、それを別の文字列に置き換える」ものではなく、「指定した範囲を、別の文字列に置き換える」というものです。文字列を探して置き換えるには、検索系の関数を組み合わせる必要があります。

replaceメンバ関数は以下のようにオーバーロードされています[9](以下の宣言は説明のためのものであり、正確に標準規格と同じではありません)。

string& replace(size_type pos1, size_type n1, const std::string& str);
string& replace(size_type pos1, size_type n1, const std::string& str, size_type pos2, size_type n2 = npos);
string& replace(size_type pos, size_type n1, const char* s, size_type n2);
string& replace(size_type pos, size_type n1, const char* s);
string& replace(size_type pos, size_type n1, size_type n, char c);
string& replace(const_iterator i1, const_iterator i2, const string& str);
string& replace(const_iterator i1, const_iterator i2, const char* s, size_type n);
string& replace(const_iterator i1, const_iterator i2, size_type n, char c);

template <class InputIterator>
string& replace(const_iterator i1, const_iterator i2, InputIterator j1, InputIterator j2);

string& replace(const_iterator i1, const_iterator i2, initializer_list<char> il);

かなり種類がありますが、ともかく仕事の内容としては「指定範囲の文字列を、別の文字列に置き換える」ことで共通しています。

仮引数pos、pos1 は置換対象の開始位置(先頭から何文字目か)、pos2 は置き換え元の文字列の開始位置です。n1 は置換対象の方の文字数、n2 は置き換え元の文字列の文字数です。str、s、c は置き換え元の文字列や文字です。置換対象の文字数と、置き換え元の文字列の文字数が一致しなくても構いません。

5つ目の形式(string& replace(size_type pos, size_type n1, size_type n, char c);)が分かりづらいですが、これは c という文字を n個並べた文字列で置き換えるというものです。

6つ目以降の形式ではイテレータを使っています。仮引数i1、i2 は置換対象の文字列内の範囲を、j1、j2 は置き換え元の文字列の範囲を指定します。template <class InputIterator> は解説していない機能ですが、詳細を知らなくても使えると思います。

10個目の形式は、置き換え元の文字列を initializer_list を使って渡します。

戻り値はいずれの形式でも、対象の std::string オブジェクトへの参照です(*this を返す)。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::string target {};
    std::string s {"vwxyz"};
    
    target = "abcdef";
    std::cout << target.replace(1, 2, s) << "\n";

    target = "abcdef";
    std::cout << target.replace(1, 2, s, 1, 2) << "\n";

    target = "abcdef";
    std::cout << target.replace(1, 2, "vwxyz", 3) << "\n";

    target = "abcdef";
    std::cout << target.replace(1, 2, "vwxyz") << "\n";

    target = "abcdef";
    std::cout << target.replace(1, 2, 4, 'x') << "\n";

    target = "abcdef";
    std::cout << target.replace(std::cbegin(target) + 1, std::cbegin(target) + 3, s) << "\n";

    target = "abcdef";
    std::cout << target.replace(std::cbegin(target) + 1, std::cbegin(target) + 3, "vwxyz", 3) << "\n";

    target = "abcdef";
    std::cout << target.replace(std::cbegin(target) + 1, std::cbegin(target) + 3, 4, 'x') << "\n";

    target = "abcdef";
    std::cout << target.replace(std::cbegin(target) + 1, std::cbegin(target) + 3, std::cbegin(s), std::cbegin(s) + 2) << "\n";

    target = "abcdef";
    std::cout << target.replace(std::cbegin(target) + 1, std::cbegin(target) + 3, {'x', 'y', 'z'}) << "\n";
}

実行結果:

avwxyzdef
awxdef
avwxdef
avwxyzdef
axxxxdef
avwxyzdef
avwxdef
axxxxdef
avwdef
axyzdef

比較 🔗

std::string の compareメンバ関数を使って、ほかの文字列との比較が行えます。ここでいう比較とは、両者が完全に同じ内容の文字列であるかどうか、そうでないのなら、どちらのほうが小さい・大きいといえるかということです。

具体的には以下のように判断します。

  1. 自身の長さと、比較対象の文字列の長さを取得し、短い方の長さを判断する(len = std::min(s.length(), other.length())
  2. len の長さ分だけ、2つの文字列内の文字を1文字ずつ比べる(s[i] と other[i] の比較)。途中で一致しない文字が現れたら、そのときの大小関係が結果となって終了(s[i] < other[i] なら s が小さい。s[i] > other[i] なら s が大きい)
  3. 手順2の途中で終了することなく、len の長さ分の比較を終えた場合は、全体の長さが短い側が小さい(s.length() < other.length() なら s が小さい。s.length() > other.length() なら s が大きい)

手順2のところで行う文字の比較とは、文字コード(「文字」のページを参照)による比較ということです。

【上級】文字の比較は具体的には、std::string のテンプレートパラメータ Traits を使って、Traits::compare() を呼び出すことで行われます。

compareメンバ関数は以下のようにオーバーロードされています[10](以下の宣言は説明のためのものであり、正確に標準規格と同じではありません)。

int compare(const basic_string& str) const;
int compare(size_type pos1, size_type n1, const basic_string& str) const;
int compare(size_type pos1, size_type n1, const basic_string& str, size_type pos2, size_type n2 = npos) const;
int compare(const charT* s) const;
int compare(size_type pos1, size_type n1, const charT* s) const;
int compare(size_type pos1, size_type n1, const charT* s, size_type n2) const;

仮引数str、s が比較対象の文字列です。pos1 は自身の何文字目から比較を開始するかという指定、n1 は比較を行う最大文字数の指定です。pos2、n2 は比較対象側の開始位置と最大文字数の指定です。文字数の指定については、std::string::npos が「末尾まですべて」を意味します。pos1、pos2 が文字列の長さを越えてはいけません。

戻り値は、比較結果が「同じ」なら 0、「自身の方が小さい」なら 0未満の値、「自身の方が大きい」なら 0 より大きい値です。「同じ」でないときに -11 が返される保証はないので、判定の仕方に注意してください。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::string s1 {"abcxyzabcxyz"};
    std::string s2 {"abc"};

    std::cout << s1.compare(s2) << "\n"
              << s1.compare(6, 3, s2) << "\n"
              << s1.compare(6, 3, s2, 1, 2) << "\n"
              << s1.compare("xyz") << "\n"
              << s1.compare(3, 3, "xyz") << "\n"
              << s1.compare(3, 3, "xyz", 1) << "\n";
}

実行結果:

1
0
-1
-1
0
1

算術型を文字列に変換する

算術型(整数型や浮動小数点型)の値を std::string に変換する関数が <string> に用意されています(std::string のメンバ関数ではありません)[11]

string to_string(int val);
string to_string(long val);
string to_string(long long val);
string to_string(unsigned val);
string to_string(unsigned long val);
string to_string(unsigned long long val);
string to_string(float val);
string to_string(double val);
string to_string(long double val);

【C言語プログラマー】これらの関数は sprintf関数(C言語編)を呼び出すように実装されています。上から順に、“%d”、“%ld”、“%lld”、“%u”、“%lu”、“%llu”、“%f”、“%f”、“%Lf” による変換を行います。

仮引数val が変換元の算術型の値です。変換結果が戻り値で返されます。

以下は使用例です。

#include <iostream>
#include <string>

int main()
{
    std::cout << std::to_string(-123) << "\n"
              << std::to_string(-1'234'567L) << "\n"
              << std::to_string(-123'456'789'012LL) << "\n"
              << std::to_string(123U) << "\n"
              << std::to_string(1'234'567UL) << "\n"
              << std::to_string(123'456'789'012ULL) << "\n"
              << std::to_string(1.23f) << "\n"
              << std::to_string(1.2345678) << "\n"
              << std::to_string(1.2345678L) << "\n";
}

実行結果:

-123
-1234567
-123456789012
123
1234567
123456789012
1.230000
1.234568
1.234568

文字列を算術型に変換する

さきほどの反対で、"-123" とか "1.23" など、数として読める形式の文字列を算術型に変換したいときがあります。この操作は std::istringstream を使う方法でもある程度実現できますが(「stringstream」のページを参照)、そもそも型の変換を行うためのものではないですし、あまり便利ではありません。

より直接的に、std::string から算術型へと変換する関数が <string> に用意されています(std::string のメンバ関数ではありません)。変換先の型ごとに以下のように別の関数になっています[12]

int stoi(const string& str, size_t* idx = nullptr, int base = 10);
long stol(const string& str, size_t* idx = nullptr, int base = 10);
long long stoll(const string& str, size_t* idx = nullptr, int base = 10);
unsigned long stoul(const string& str, size_t* idx = nullptr, int base = 10);
unsigned long long stoull(const string& str, size_t* idx = nullptr, int base = 10);
float stof(const string& str, size_t* idx = nullptr);
double stod(const string& str, size_t* idx = nullptr);
long double stold(const string& str, size_t* idx = nullptr);

戻り値が unsigned int型のものは存在しません。

仮引数str が対象の文字列です。戻り値は変換された結果です。

base には基数を 2~36 の範囲あるいは 0 で指定します。0 を指定した場合は str の先頭の文字が 0 なら 8進数、0x0X なら 16進数、いずれでもなければ 10進数として扱われます。

変換は str の先頭から順番に、変換できない文字が現れるか末尾に到達するまで続きます。仮引数idx に nullptr 以外を指定している場合は、変換できなかった最初の文字のアドレスが格納されます。

【C言語プログラマー】C言語には strtol関数(C言語編)などの変換関数がありますが、上記の関数はこうしたC言語の関数を呼び出しています。

算術型から文字列に変換する場合とは違って、こちらは失敗する可能性があるため難しくなっています。以下のような状況で失敗する可能性があります。

  1. 数値とみなせる文字がなく、変換が行なえなかったとき
  2. 変換した結果が、戻り値の型で表現できないとき

上記の理由による失敗は、C++ の例外 (exception) という機能によって通知されます。ここでは詳しい解説は避けますが、次のようにコードを書けば、変換の失敗を検知できます。

#include <iostream>
#include <string>

void stoi_test(const std::string& s)
{
    try {
        int result = std::stoi(s);
        std::cout << "変換結果: " << result << "\n";
    }
    catch (const std::invalid_argument&) {
        std::cout << "変換できません。\n";
    }
    catch (const std::out_of_range&) {
        std::cout << "変換結果が表現できません。\n";
    }
}

int main()
{
    stoi_test("123");
    stoi_test("xyz");
    stoi_test("111111111111111111");
}

実行結果:

変換結果: 123
変換できません。
変換結果が表現できません。

例外による失敗の通知を行う可能性がある範囲を try {} で囲んでおきます。実際に失敗すると、失敗の理由ごとに定められている型の値とともに、その型に対応した catch () のブロックに移動します。先ほど挙げた失敗の理由1であれば std::invalid_argument という型の値が、理由2であれば std::out_of_range という型の値がそれぞれ発生します。そのため、std::stoi関数に "xyz" を渡すと、変換できる部分がまったくないため catch (const std::invalid_argument&) のブロックへ飛びます。"111111111111111111" を渡すと、変換結果が int型で表現できない数になってしまうため catch (const std::out_of_range&) のブロックへ飛びます。catch () のブロック内のコードが実行されたあとは、例外の発生元に戻るわけではなく、単にブロックの後ろに進みます(ほかの catch () に侵入することはありません)。

例外の仕組みには、try や catch のブロックが呼び出し元の関数の側にあってもいいという特徴があります。

#include <iostream>
#include <string>

void stoi_test(const std::string& s)
{
    int result = std::stoi(s);
    std::cout << "変換結果: " << result << "\n";
}

int main()
{
    try {
        stoi_test("123");
        stoi_test("xyz");
        stoi_test("111111111111111111");
    }
    catch (const std::invalid_argument&) {
        std::cout << "変換できません。\n";
    }
    catch (const std::out_of_range&) {
        std::cout << "変換結果が表現できません。\n";
    }
}

実行結果:

変換結果: 123
変換できません。

std::stoi関数に "xyz" を渡して失敗した時点で、catch (const std::invalid_argument&) のブロックに飛んでいるので、「変換できません」の出力後はそのまま main関数の終端に達するため、以降には何も出力されていないことにも注目しておきましょう。


ペイントスクリプトでも、文字列として入力された数値を取り出す必要があって、to_int関数という変換関数を自作しました(「静的メンバ」のページの練習問題を参照)。この実装では、整数に変換できない間違った入力があったときに、0 とみなしてコマンドの実行が継続されていました。std::stoi関数を使うように置き換えて、例外の処理も記述してやると、間違った入力を検知してエラーメッセージを出力し、そのコマンドは実行せずに次の入力を待ち受ける状態に戻すことも可能になります。

以下に、その置き換えも行ったあとのペイントスクリプトの全体像を掲載します。これでペイントスクリプトは一旦の完成ということになります。

//main.cpp
#include <iostream>
#include "canvas.h"
#include "command_executor.h"
#include "string_util.h"

int main()
{
    std::cout << "コマンドを入力してください。\n"
              << "help と入力すると、コマンドの一覧を表示します。\n"
              << "exit と入力すると、プログラムを終了します。\n";

    paint_script::Canvas canvas {};
    paint_script::CommandExecutor executor {canvas};

    while (true) {
        std::string input_string {};
        std::getline(std::cin, input_string);
        
        // 入力内容を空白文字ごとに分割して、std::vector に格納する
        const auto command_vec = str_util::split(input_string, " ");
        if (command_vec.empty()) {
            continue;
        }
        
        // 該当するコマンドを探して実行
        if (executor.exec(command_vec) == paint_script::CommandExecutor::ExecResult::exit_program) {
            break;
        }
    }
}
// string_util.cpp
#include "string_util.h"

namespace str_util {

    // 文字列を分割する
    std::vector<std::string> split(const std::string& str, const std::string& delim)
    {
        const auto delimLen = delim.size();
        if (delimLen == 0) {
            return {str};
        }

        std::string::size_type current {0};
        std::string::size_type found {};
        std::vector<std::string> result {};

        // 区切り文字を探しながら、その位置の手前までの文字列を追加することを繰り返す
        while ((found = str.find(delim, current)) != std::string::npos) {
            result.push_back(str.substr(current, found - current));
            current = found + delimLen;
        }

        // 残った部分を追加
        result.push_back(str.substr(current, str.size() - current));

        return result;
    }

}
// string_util.h
#ifndef STRING_UTIL_H_INCLUDED
#define STRING_UTIL_H_INCLUDED

#include <string>
#include <vector>

namespace str_util {

    // 文字列を分割する
    //
    // str:   対象の文字列
    // delim: 区切り文字列
    // 戻り値: str を delim で区切った部分文字列を格納した vector
    std::vector<std::string> split(const std::string& str, const std::string& delim);

}

#endif
// canvas.cpp
#include "canvas.h"
#include "bmp.h"
#include "pen.h"
#include <algorithm>
#include <cassert>
#include <cstdlib>

namespace paint_script {

    Canvas::Canvas(unsigned int width, unsigned int height, Color color) :
        m_pixels {},
        m_width {0},
        m_height {0}
    {
        resize(width, height, color);
    }

    void Canvas::resize(unsigned int width, unsigned int height, Color color)
    {
        assert(1 <= width);
        assert(1 <= height);

        m_pixels.resize(height);
        for (auto& row : m_pixels) {
            row.resize(width);
        }

        m_width = width;
        m_height = height;

        fill(color);
    }

    void Canvas::fill(Color color)
    {
        for (auto& row : m_pixels) {
            std::fill(std::begin(row), std::end(row), color);
        }
    }

    void Canvas::paint_dot(int x, int y, Pen& pen)
    {
        if (!is_inside(x, y)) {
            return;
        }

        m_pixels[y][x] = pen.get_color();
    }

    // 矩形を描画する
    void Canvas::paint_rect(int left, int top, int right, int bottom, Pen& pen)
    {
        // 上辺
        for (int x {left}; x <= right; ++x) {
            paint_dot(x, top, pen);
        }

        // 左辺
        for (int y {top + 1}; y < bottom; ++y) {
            paint_dot(left, y, pen);
        }

        // 右辺
        for (int y {top + 1}; y < bottom; ++y) {
            paint_dot(right, y, pen);
        }

        // 下辺
        for (int x {left}; x <= right; ++x) {
            paint_dot(x, bottom, pen);
        }
    }

    // 内側を塗りつぶした矩形を描画する
    void Canvas::paint_filled_rect(int left, int top, int right, int bottom, Pen& pen)
    {
        for (int y {top}; y <= bottom; ++y) {
            for (int x {left}; x <= right; ++x) {
                paint_dot(x, y, pen);
            }
        }
    }

    bool Canvas::load_from_bitmap_file(const std::string& path)
    {
        if (!Bmp::load(path, &m_width, &m_height, &m_pixels)) {
            return false;
        }

        return true;
    }

    bool Canvas::save_to_bitmap_file(const std::string& path)
    {
        return Bmp::save(path, m_width, m_height, m_pixels);
    }

    bool Canvas::is_inside(int x, int y) const
    {
        if (x < 0 || static_cast<int>(m_width) <= x) {
            return false;
        }
        if (y < 0 || static_cast<int>(m_height) <= y) {
            return false;
        }
        return true;
    }

}
// canvas.h
#ifndef CANVAS_H_INCLUDED
#define CANVAS_H_INCLUDED

#include <string>
#include <vector>
#include "color.h"

namespace paint_script {

    class Pen;

    class Canvas {
    public:
        static constexpr unsigned int default_width {320};
        static constexpr unsigned int default_height {240};

    public:
        // コンストラクタ
        //
        // width: 横方向のピクセル数。省略時は default_width
        // height: 縦方向のピクセル数。省略時は default_height
        // color: 初期状態の色。省略時は白
        Canvas(unsigned int width = default_width, unsigned int height = default_height, Color color = {255, 255, 255});

    public:
        // キャンバスの大きさを変更する
        // 
        // これまでのキャンバスに描かれていた内容は失われ、
        // color の色で塗りつぶされる。
        //
        // width: 横方向のピクセル数 (1~WidthMax)
        // height: 縦方向のピクセル数 (1~HeightMax)
        // color: 色。省略時は白
        void resize(unsigned int width, unsigned int height, Color color = {255, 255, 255});


        // 全面を塗りつぶす
        //
        // color: 色
        void fill(Color color);


        // 点を描画する
        //
        // x: X座標
        // y: Y座標
        // pen: ペン
        void paint_dot(int x, int y, Pen& pen);

        // 矩形を描画する
        //
        // left: 左端X座標
        // top: 上端Y座標
        // right: 右端X座標
        // bottom: 下端Y座標
        // pen: ペン
        void paint_rect(int left, int top, int right, int bottom, Pen& pen);

        // 内側を塗りつぶした矩形を描画する
        //
        // left: 左端X座標
        // top: 上端Y座標
        // right: 右端X座標
        // bottom: 下端Y座標
        // pen: ペン
        void paint_filled_rect(int left, int top, int right, int bottom, Pen& pen);


        // ビットマップファイルから読み込む
        //
        // path: ビットマップファイルのパス
        // 戻り値: 成否
        bool load_from_bitmap_file(const std::string& path);

        // ビットマップファイルとして書き出す
        //
        // path: 出力先のビットマップファイルのパス
        // 戻り値: 成否
        bool save_to_bitmap_file(const std::string& path);



        // 横方向のピクセル数を返す
        //
        // 戻り値: 横方向のピクセル数
        inline unsigned int get_width() const
        {
            return m_width;
        }
        
        // 縦方向のピクセル数を返す
        //
        // 戻り値: 縦方向のピクセル数
        inline unsigned int get_height() const
        {
            return m_height;
        }

        // 座標がキャンバスの範囲内かどうか調べる
        //
        // x: X座標
        // y: Y座標
        // 戻り値: キャンバス内の座標なら true。そうでなければ false
        bool is_inside(int x, int y) const;

    private:
        std::vector<std::vector<Color>>     m_pixels;
        unsigned int                        m_width;
        unsigned int                        m_height;
    };

}

#endif
// command_executor.cpp
#include "command_executor.h"
#include "canvas.h"
#include "string_util.h"
#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <iterator>


namespace paint_script {

    const CommandExecutor::CommandData CommandExecutor::CommandMap[] {
        {"help", &help, &print_help_help},
        {"exit", &exit, &print_help_exit},
        {"resize", &resize, &print_help_resize},
        {"fill", &fill, &print_help_fill},
        {"pen", &pen, &print_help_pen},
        {"dot", &dot, &print_help_dot},
        {"rect", &rect, &print_help_rect},
        {"filled_rect", &filled_rect, &print_help_filled_rect},
        {"load", &load, &print_help_load},
        {"save", &save, &print_help_save},
    };



    CommandExecutor::CommandExecutor(Canvas& canvas) :
        m_canvas {canvas}
    {

    }

    CommandExecutor::ExecResult CommandExecutor::exec(const command_params_t& command_vec)
    {
        const std::string command_name {command_vec.at(0)};

        // コマンドを探す
        const auto command_it = std::find_if(
            std::cbegin(CommandMap),
            std::cend(CommandMap),
            [command_name](const CommandData& data) { return data.name == command_name; });
        if (command_it == std::cend(CommandMap)) {
            return ExecResult::not_found;
        }
        
        // 実行
        try {
            return command_it->impl(this, command_vec);
        }
        catch (const std::invalid_argument&) {
            std::cout << "入力に間違いがあります。\n";
        }
        catch (const std::out_of_range&) {
            std::cout << "有効範囲を越えた入力があります。\n";
        }
        return ExecResult::failed;
    }


    // ヘルプ
    CommandExecutor::ExecResult CommandExecutor::help(const command_params_t&)
    {
        print_help();
        return ExecResult::successd;
    }

    // 終了
    CommandExecutor::ExecResult CommandExecutor::exit(const command_params_t&)
    {
        return ExecResult::exit_program;
    }

    // キャンバスの大きさを変更する
    CommandExecutor::ExecResult CommandExecutor::resize(const command_params_t& cmd_vec)
    {
        if (cmd_vec.size() < 3) {
            std::cout << "resize コマンドには2つのパラメータが必要です。\n";
            print_help_resize();
            return ExecResult::failed;
        }

        const int width {std::stoi(cmd_vec.at(1))};
        if (width < 1) {
            std::cout << "横方向のピクセル数は 1 以上でなければなりません。\n";
            print_help_resize();
            return ExecResult::failed;
        }

        const int height {std::stoi(cmd_vec.at(2))};
        if (height < 1) {
            std::cout << "縦方向のピクセル数は 1 以上でなければなりません。\n";
            print_help_resize();
            return ExecResult::failed;
        }

        m_canvas.resize(static_cast<unsigned int>(width), static_cast<unsigned int>(height));
        return ExecResult::successd;
    }
        
    // キャンバスを塗りつぶす
    CommandExecutor::ExecResult CommandExecutor::fill(const command_params_t& cmd_vec)
    {
        if (cmd_vec.size() < 4) {
            std::cout << "fill コマンドには3つのパラメータが必要です。\n";
            print_help_fill();
            return ExecResult::failed;
        }

        const int red {std::stoi(cmd_vec.at(1))};
        if (red < 0 || 255 < red) {
            std::cout << "赤成分の強さは 0 から 255 の範囲でなければなりません。\n";
            print_help_fill();
            return ExecResult::failed;
        }

        const int green {std::stoi(cmd_vec.at(2))};
        if (green < 0 || 255 < green) {
            std::cout << "緑成分の強さは 0 から 255 の範囲でなければなりません。\n";
            print_help_fill();
            return ExecResult::failed;
        }

        const int blue {std::stoi(cmd_vec.at(3))};
        if (blue < 0 || 255 < blue) {
            std::cout << "青成分の強さは 0 から 255 の範囲でなければなりません。\n";
            print_help_fill();
            return ExecResult::failed;
        }

        m_canvas.fill({static_cast<unsigned char>(red), static_cast<unsigned char>(green), static_cast<unsigned char>(blue)});
        return ExecResult::successd;
    }

    // ペンを変更する
    CommandExecutor::ExecResult CommandExecutor::pen(const command_params_t& cmd_vec)
    {
        if (cmd_vec.size() < 4) {
            std::cout << "pen コマンドには3つのパラメータが必要です。\n";
            print_help_pen();
            return ExecResult::failed;
        }

        const int red {std::stoi(cmd_vec.at(1))};
        if (red < 0 || 255 < red) {
            std::cout << "赤成分の強さは 0 から 255 の範囲でなければなりません。\n";
            print_help_pen();
            return ExecResult::failed;
        }

        const int green {std::stoi(cmd_vec.at(2))};
        if (green < 0 || 255 < green) {
            std::cout << "緑成分の強さは 0 から 255 の範囲でなければなりません。\n";
            print_help_pen();
            return ExecResult::failed;
        }

        const int blue {std::stoi(cmd_vec.at(3))};
        if (blue < 0 || 255 < blue) {
            std::cout << "青成分の強さは 0 から 255 の範囲でなければなりません。\n";
            print_help_pen();
            return ExecResult::failed;
        }

        const Pen pen({static_cast<unsigned char>(red), static_cast<unsigned char>(green), static_cast<unsigned char>(blue)});
        m_pen = pen;
        return ExecResult::successd;
    }

    // 点を描画する
    CommandExecutor::ExecResult CommandExecutor::dot(const command_params_t& cmd_vec)
    {
        if (cmd_vec.size() < 3) {
            std::cout << "dot コマンドには2つのパラメータが必要です。\n";
            print_help_dot();
            return ExecResult::failed;
        }

        const int x {std::stoi(cmd_vec.at(1))};
        const int y {std::stoi(cmd_vec.at(2))};

        m_canvas.paint_dot(x, y, m_pen);
        return ExecResult::successd;
    }

    // 矩形を描画する
    CommandExecutor::ExecResult CommandExecutor::rect(const command_params_t& cmd_vec)
    {
        if (cmd_vec.size() < 5) {
            std::cout << "rect コマンドには4つのパラメータが必要です。\n";
            print_help_rect();
            return ExecResult::failed;
        }

        const int left {std::stoi(cmd_vec.at(1))};
        const int top {std::stoi(cmd_vec.at(2))};
        const int right {std::stoi(cmd_vec.at(3))};
        const int bottom {std::stoi(cmd_vec.at(4))};

        if (left > right) {
            std::cout << "left は right より左になければなりません。\n";
            return ExecResult::failed;
        }
        if (top > bottom) {
            std::cout << "top は bottom より上になければなりません。\n";
            return ExecResult::failed;
        }

        m_canvas.paint_rect(left, top, right, bottom, m_pen);
        return ExecResult::successd;
    }

    // 矩形を描画し、内側を塗りつぶす
    CommandExecutor::ExecResult CommandExecutor::filled_rect(const command_params_t& cmd_vec)
    {
        if (cmd_vec.size() < 5) {
            std::cout << "filled_rect コマンドには4つのパラメータが必要です。\n";
            print_help_filled_rect();
            return ExecResult::failed;
        }

        const int left {std::stoi(cmd_vec.at(1))};
        const int top {std::stoi(cmd_vec.at(2))};
        const int right {std::stoi(cmd_vec.at(3))};
        const int bottom {std::stoi(cmd_vec.at(4))};

        if (left > right) {
            std::cout << "left は right より左になければなりません。\n";
            return ExecResult::failed;
        }
        if (top > bottom) {
            std::cout << "top は bottom より上になければなりません。\n";
            return ExecResult::failed;
        }

        m_canvas.paint_filled_rect(left, top, right, bottom, m_pen);
        return ExecResult::successd;
    }

    // ビットマップファイルから読み込む
    CommandExecutor::ExecResult CommandExecutor::load(const command_params_t& cmd_vec)
    {
        if (cmd_vec.size() < 2) {
            std::cout << "load コマンドには1つのパラメータが必要です。\n";
            print_help_load();
            return ExecResult::failed;
        }

        const std::string path {cmd_vec.at(1)};

        if (!m_canvas.load_from_bitmap_file(path)) {
            std::cout << "path " << "の読み込みに失敗しました。\n";
            return ExecResult::failed;
        }
        return ExecResult::successd;
    }

    // ビットマップファイルに保存する
    CommandExecutor::ExecResult CommandExecutor::save(const command_params_t& cmd_vec)
    {
        if (cmd_vec.size() < 2) {
            std::cout << "save コマンドには1つのパラメータが必要です。\n";
            print_help_save();
            return ExecResult::failed;
        }

        const std::string path {cmd_vec.at(1)};

        if (!m_canvas.save_to_bitmap_file(path)) {
            std::cout << "path " << "への保存に失敗しました。\n";
            return ExecResult::failed;
        }
        return ExecResult::successd;
    }


    void CommandExecutor::print_help() const
    {
        std::cout << "以下のコマンドがあります。\n"
                  << "対応するパラメータがある場合は、その順番どおりに、正しい値をスペースで区切って入力してください。\n"
                  << std::endl;

        for (const auto& data : CommandMap) {
            data.help(this);
        }
    }

    void CommandExecutor::print_help_help() const
    {
        std::cout << "help\n"
                  << "ヘルプメッセージを出力します。\n"
                  << std::endl;
    }

    void CommandExecutor::print_help_exit() const
    {
        std::cout << "exit\n"
                  << "スクリプトの実行を終了します。\n"
                  << std::endl;
    }

    void CommandExecutor::print_help_resize() const
    {
        std::cout << "resize width height\n"
                  << "キャンバスの大きさを変更します。\n"
                  << "  width:  横方向のピクセル数を 1 以上の大きさで指定します。\n"
                  << "  height: 縦方向のピクセル数を 1 以上の大きさで指定します。\n"
                  << std::endl;
    }

    void CommandExecutor::print_help_fill() const
    {
        std::cout << "fill red green blue\n"
                  << "キャンバスを1色で塗りつぶします。\n"
                  << "  red:   赤成分の強さを 0 から 255 の範囲で指定します。\n"
                  << "  green: 緑成分の強さを 0 から 255 の範囲で指定します。\n"
                  << "  blue:  青成分の強さを 0 から 255 の範囲で指定します。\n"
                  << std::endl;
    }

    void CommandExecutor::print_help_pen() const
    {
        std::cout << "pen red green blue\n"
                  << "点や線を描くときに使うペンを変更します。\n"
                  << "  red:   赤成分の強さを 0 から 255 の範囲で指定します。\n"
                  << "  green: 緑成分の強さを 0 から 255 の範囲で指定します。\n"
                  << "  blue:  青成分の強さを 0 から 255 の範囲で指定します。\n"
                  << std::endl;
    }

    void CommandExecutor::print_help_dot() const
    {
        std::cout << "dot x y\n"
                  << "現在のペンを使って、点を描画します。\n"
                  << "  x: X座標を指定します。キャンバスの範囲外の場合は何も描かれません。\n"
                  << "  y: Y座標を指定します。キャンバスの範囲外の場合は何も描かれません。\n"
                  << std::endl;
    }

    void CommandExecutor::print_help_rect() const
    {
        std::cout << "rect left top right bottom\n"
                  << "現在のペンを使って、矩形を描画します。\n"
                  << "内側は塗られません。内側を塗る場合は、filled_rect コマンドを使用してください。\n"
                  << "  left:   左端のX座標を指定します。キャンバスの範囲外も指定できます。\n"
                  << "  top:    上端のY座標を指定します。キャンバスの範囲外も指定できます。\n"
                  << "  right:  右端のX座標を指定します。キャンバスの範囲外も指定できます。\n"
                  << "  bottom: 下端のY座標を指定します。キャンバスの範囲外も指定できます。\n"
                  << std::endl;
    }

    void CommandExecutor::print_help_filled_rect() const
    {
        std::cout << "filled_rect left top right bottom\n"
                  << "現在のペンを使って、矩形を描画します。\n"
                  << "内側を塗りつぶします。内側を塗らない場合は、rect コマンドを使用してください。\n"
                  << "  left:   左端のX座標を指定します。キャンバスの範囲外も指定できます。\n"
                  << "  top:    上端のY座標を指定します。キャンバスの範囲外も指定できます。\n"
                  << "  right:  右端のX座標を指定します。キャンバスの範囲外も指定できます。\n"
                  << "  bottom: 下端のY座標を指定します。キャンバスの範囲外も指定できます。\n"
                  << std::endl;
    }

    void CommandExecutor::print_help_load() const
    {
        std::cout << "load path\n"
                  << ".bmpファイルを指定して、キャンバスを作成します。\n"
                  << "  path: 読み込む .bmpファイルのパス。\n"
                  << std::endl;
    }

    void CommandExecutor::print_help_save() const
    {
        std::cout << "save path\n"
                  << "現在のキャンバスの状態を .bmp ファイルに保存します。\n"
                  << "  path: 出力する .bmpファイルのパス。すでに存在する場合は上書きします。\n"
                  << std::endl;
    }
}
// command_executor.h
#ifndef COMMAND_EXECUTOR_H_INCLUDED
#define COMMAND_EXECUTOR_H_INCLUDED

#include <functional>
#include <string>
#include <vector>
#include "pen.h"

namespace paint_script {

    class Canvas;

    class CommandExecutor {
    public:
        using command_params_t = std::vector<std::string>;  // コマンドとパラメータの型

        // 結果
        enum class ExecResult {
            successd,       // 成功
            failed,         // 失敗
            not_found,      // コマンドが見つからない
            exit_program,   // 成功。プログラムを終了させる
        };

    public:
        // コンストラクタ
        //
        // canvas: キャンバスの参照
        explicit CommandExecutor(Canvas& canvas);

    public:
        // コマンドを実行する
        //
        // command_vec: コマンドとパラメータを含んだ配列
        // 戻り値: 結果
        ExecResult exec(const command_params_t& command_vec);

    private:
        using CommandImpl_t = std::function<ExecResult (CommandExecutor*, const command_params_t&)>;    // 実装関数の型   
        using CommandHelp_t = std::function<void (const CommandExecutor*)>;                     // ヘルプ出力関数の型

        // コマンドデータ
        struct CommandData {
            const char*     name;   // コマンド名
            CommandImpl_t   impl;   // コマンドの実装関数
            CommandHelp_t   help;   // コマンドのヘルプを出力する関数
        };
        static const CommandData CommandMap[];

    private:
        ExecResult help(const command_params_t& cmd_vec);
        ExecResult exit(const command_params_t& cmd_vec);
        ExecResult resize(const command_params_t& cmd_vec);
        ExecResult fill(const command_params_t& cmd_vec);
        ExecResult pen(const command_params_t& cmd_vec);
        ExecResult dot(const command_params_t& cmd_vec);
        ExecResult rect(const command_params_t& cmd_vec);
        ExecResult filled_rect(const command_params_t& cmd_vec);
        ExecResult load(const command_params_t& cmd_vec);
        ExecResult save(const command_params_t& cmd_vec);

        void print_help() const;
        void print_help_help() const;
        void print_help_exit() const;
        void print_help_resize() const;
        void print_help_fill() const;
        void print_help_pen() const;
        void print_help_dot() const;
        void print_help_rect() const;
        void print_help_filled_rect() const;
        void print_help_load() const;
        void print_help_save() const;

    private:
        Canvas&                 m_canvas;
        Pen                     m_pen {{0, 0, 0}};
    };

}

#endif
// pen.cpp
#include "pen.h"

namespace paint_script {

    Pen::Pen(Color color) :
        m_color {color}
    {

    }

}
// pen.h
#ifndef PEN_H_INCLUDED
#define PEN_H_INCLUDED

#include "color.h"

namespace paint_script {

    class Pen {
    public:
        // コンストラクタ
        //
        // color: 色
        explicit Pen(Color color);

    public:
        // 色を返す
        //
        // 戻り値: 色
        inline Color get_color() const
        {
            return m_color;
        }

    private:
        Color           m_color;
    };
}

#endif
// color.h
#ifndef COLOR_H_INCLUDED
#define COLOR_H_INCLUDED

namespace paint_script {

    // 色
    struct Color {
        unsigned char  red;         // 赤成分
        unsigned char  green;       // 緑成分
        unsigned char  blue;        // 青成分
    };
}

#endif
// bmp.cpp
#include "bmp.h"
#include "color.h"
#include <cassert>
#include <cstdint>
#include <fstream>

namespace paint_script {

    bool Bmp::save(const std::string& path, unsigned int width, unsigned int height, const std::vector<std::vector<Color>>& pixels)
    {
        std::ofstream ofs {path, std::ios_base::out | std::ios_base::binary};
        if (!ofs) {
            return false;
        }

        // ----- ファイルヘッダ部 -----
        // Windows API の BITMAPFILEHEADER構造体にあたる。
        // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapfileheader

        // ファイルタイプ
        // 必ず 0x4d42 ("BM" のこと)
        std::uint16_t file_type {0x4d42};
        ofs.write(reinterpret_cast<const char*>(&file_type), sizeof(file_type));

        // ファイルサイズ
        // 縦横のピクセル数 * 1ピクセル当たりのバイト数(PaintScript では 4バイト固定) + ヘッダ部のバイト数。
        // ヘッダ部の大きさは、sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) より。
        std::uint32_t file_size {width * height * 4 + 54};
        ofs.write(reinterpret_cast<const char*>(&file_size), sizeof(file_size));

        // 予約領域
        std::uint32_t reserved {0};
        ofs.write(reinterpret_cast<const char*>(&reserved), sizeof(reserved));

        // ファイル先頭から、ピクセル情報までの距離
        std::uint32_t offset_to_pixels {54};
        ofs.write(reinterpret_cast<const char*>(&offset_to_pixels), sizeof(offset_to_pixels));


        // ----- ビットマップ情報ヘッダ部 -----
        // Windows API の _BITMAPINFOHEADER構造体にあたる。
        // https://learn.microsoft.com/ja-jp/windows/win32/wmdm/-bitmapinfoheader

        // ビットマップ情報ヘッダ部のサイズ
        std::uint32_t bitmap_info_header_size {40};
        ofs.write(reinterpret_cast<const char*>(&bitmap_info_header_size), sizeof(bitmap_info_header_size));

        // 横方向のピクセル数
        std::int32_t w {static_cast<std::int32_t>(width)};
        ofs.write(reinterpret_cast<const char*>(&w), sizeof(w));

        // 縦方向のピクセル数
        std::int32_t h {static_cast<std::int32_t>(height)};
        ofs.write(reinterpret_cast<const char*>(&h), sizeof(h));

        // プレーン数。必ず 1
        std::uint16_t planes {1};
        ofs.write(reinterpret_cast<const char*>(&planes), sizeof(planes));

        // 1ピクセル当たりのビット数。PaintScript では 24 に固定
        std::uint16_t bit_count {24};
        ofs.write(reinterpret_cast<const char*>(&bit_count), sizeof(bit_count));

        // 圧縮形式。無圧縮は 0
        std::uint32_t compression {0};
        ofs.write(reinterpret_cast<const char*>(&compression), sizeof(compression));

        // 画像サイズ。無圧縮であれば 0 で構わない
        std::uint32_t image_size {0};
        ofs.write(reinterpret_cast<const char*>(&image_size), sizeof(image_size));

        // メートル当たりの横方向のピクセル数の指示。不要なら 0 にできる
        std::int32_t x_pixels_per_meter {0};
        ofs.write(reinterpret_cast<const char*>(&x_pixels_per_meter), sizeof(x_pixels_per_meter));

        // メートル当たりの縦方向のピクセル数の指示。不要なら 0 にできる
        std::int32_t y_pixels_per_meter {0};
        ofs.write(reinterpret_cast<const char*>(&y_pixels_per_meter), sizeof(y_pixels_per_meter));

        // カラーテーブル内の色のうち、実際に使用している個数。パレット形式でなければ無関係
        std::uint32_t clr_used {0};
        ofs.write(reinterpret_cast<const char*>(&clr_used), sizeof(clr_used));

        // カラーテーブル内の色のうち、重要色である色の個数。パレット形式でなければ無関係
        std::uint32_t clr_important {0};
        ofs.write(reinterpret_cast<const char*>(&clr_important), sizeof(clr_important));


        // ----- ピクセル情報 -----
        // Windows API の RGBQUAD に当たる。
        // https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/ns-wingdi-rgbquad
        for (std::int32_t y {h - 1}; y >= 0; --y) {
            for (std::int32_t x {0}; x < w; ++x) {
                const Color pixel {pixels.at(y).at(x)};
                ofs.write(reinterpret_cast<const char*>(&pixel.blue), sizeof(pixel.blue));
                ofs.write(reinterpret_cast<const char*>(&pixel.green), sizeof(pixel.green));
                ofs.write(reinterpret_cast<const char*>(&pixel.red), sizeof(pixel.red));
            }
        }

        return true;
    }

    bool Bmp::load(const std::string& path, unsigned int* width, unsigned int* height, std::vector<std::vector<Color>>* pixels)
    {
        assert(width);
        assert(height);
        assert(pixels);

        std::ifstream ifs {path, std::ios_base::in | std::ios_base::binary};
        if (!ifs) {
            return false;
        }

        // 不要なところを読み飛ばす
        ifs.seekg(18, std::ios_base::beg);

        // 横方向のピクセル数
        std::int32_t w {};
        ifs.read(reinterpret_cast<char*>(&w), sizeof(w));
        if (w < 1) {
            return false;
        }
        *width = static_cast<unsigned int>(w);
        
        // 縦方向のピクセル数
        std::int32_t h {};
        ifs.read(reinterpret_cast<char*>(&h), sizeof(h));
        if (h < 1) {
            return false;
        }
        *height = static_cast<unsigned int>(h);

        // 不要なところを読み飛ばす
        ifs.seekg(28, std::ios_base::cur);

        // ピクセル情報
        // vector は、ビットマップの大きさに合わせて resize する。
        pixels->resize(h);
        for (auto& row : *pixels) {
            row.resize(w);
        }
        for (std::int32_t y {h - 1}; y >= 0; --y) {
            for (std::int32_t x {0}; x < w; ++x) {
                std::uint8_t b {};
                ifs.read(reinterpret_cast<char*>(&b), sizeof(b));
                std::uint8_t g {};
                ifs.read(reinterpret_cast<char*>(&g), sizeof(g));
                std::uint8_t r {};
                ifs.read(reinterpret_cast<char*>(&r), sizeof(r));

                pixels->at(y).at(x) = Color{r, g, b};
            }
        }

        return true;
    }
}
// bmp.h
#ifndef BMP_H_INCLUDED
#define BMP_H_INCLUDED

#include <string>
#include <vector>

namespace paint_script {

    struct Color;

    class Bmp {
    public:
        Bmp() = delete;

        // ファイルに書き出す
        //
        // path: ファイルパス
        // width: 横方向のピクセル数
        // height: 縦方向のピクセル数
        // pixels: ピクセル情報
        // 戻り値: 成否
        static bool save(const std::string& path, unsigned int width, unsigned int height, const std::vector<std::vector<Color>>& pixels);

        // ファイルから読み込む
        //
        // path: ファイルパス
        // width: 横方向のピクセル数を受け取るポインタ。ヌルポインタ不可
        // height: 縦方向のピクセル数を受け取るポインタ。ヌルポインタ不可
        // pixels: ピクセル情報を受け取るポインタ。ヌルポインタ不可
        // 戻り値: 成否
        static bool load(const std::string& path, unsigned int* width, unsigned int* height, std::vector<std::vector<Color>>* pixels);
    };

}

#endif

まとめ 🔗


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


参考リンク 🔗


練習問題 🔗

問題の難易度について。

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

問題1 (基本★)

std::string から指定の文字列を探して、別の文字列に置き換える関数を作成してください。

解答・解説

問題2 (応用★★)

ペイントスクリプトに、マクロの機能を実装してください。たとえば、色の指定を “255 0 0” と入力する代わりに “RED” と入力できるようにします。ここでは以下の事前に定義されたマクロだけを使えるものします。

マクロ 置換結果
RED 255 0 0
GREEN 0 255 0
BLUE 0 0 255
WHITE 255 255 255
BLACK 0 0 0

解答・解説

問題3 (応用★★)

ペイントスクリプトに、これまで入力したコマンドとパラメータの履歴をテキストファイルに書き出す save_scriptコマンドを実装してください。

解答・解説

問題4 (発展★★★)

ペイントスクリプトに load_scriptコマンドを実装してください。load_scriptコマンドは、load_script script.txt のように、パラメータでファイルのパスを指定し、そのファイルに書かれているスクリプトを実行します。そのファイルは手動で作ったものでも、問題3で作成した save_scriptコマンドで書き出したテキストファイルでも受け付けられるようにしてください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




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