静的メンバ 解答ページ | Programming Place Plus 新C++編

トップページ新C++編静的メンバ

このページの概要

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



解答・解説

問題1 (確認★)

次のプログラムの中から、エラーになるところをすべて挙げてください。

#include <iostream>

class C {
public:
    explicit C(int v) : m_v(v)
    {
        ms_v += v;
    }

    void f()
    {
        std::cout << "called f()\n"
                  << m_v << "\n"
                  << ms_v << "\n";
    }

    static void sf()
    {
        std::cout << "called sf()\n"
                  << m_v << "\n"
                  << ms_v << "\n";
    }

private:
    int         m_v;
    static int  ms_v;
};

int C::ms_v {100};

int main()
{
    C c(5);
    c.f();
    C::sf();
}


各メンバが、オブジェクトごとに存在するものなのか、クラスに対してたった1つだけ存在するものなのかを理解することが重要です。後者のようなメンバを静的メンバといいます(本編解説)。静的メンバは、その宣言時に static が付加されています。また、ここには登場していませんが、クラス定義の中で型の定義をしているときは、その型もクラスに対して存在するものとみなせます(本編解説)。

オブジェクトごとに存在するメンバの場合、つねに「どのオブジェクトに対して」ということを意識しなければなりません。クラスの外側から使用するときなら c.f() とか pc->f() のような記述になりますし、クラスの内側から使用するときなら this->f() のように thisポインタがオブジェクトを指すことになります(this-> は省略できるので明示的に書かないことがほとんどですが、ソースコード上では隠れているというだけです)。

クラスに対して1つだけ存在する静的メンバの場合は、つねに「どのクラスに対して」を意識します。クラスの外側から使用するなら C::sf() のようになります。そのクラス自身の内側にいるなら、sf() とするだけで判断できます。静的メンバにはその対象となるオブジェクトの存在がなく、静的メンバ関数の本体では thisポインタを使うこともできません。

これらを踏まえると、問題のプログラムの中でエラーになるのは以下の箇所です。

静的メンバ関数である sf はクラスごとの存在であるのに対し、m_v は静的でないデータメンバなので、オブジェクトごとに存在するものです。sf を呼び出す方法は C::sf() のようなものであり、対象となったオブジェクトは存在しません。ですから m_v という記述からは「どのオブジェクトに対して」が不明です。

問題2 (確認★)

静的データメンバの存在が、オブジェクトの大きさに影響を与えないことを確認してください。


オブジェクトの大きさは sizeof演算子を使って確認できます。適当なクラスを用意して調べてみましょう。

#include <iostream>

class C {
private:
    int m_value;
    static int ms_values[1000];
};

int C::ms_values[1000];

int main()
{
    C c {};
    std::cout << sizeof(c) << "\n";
}

実行結果:

4

要素数 1000 の静的データメンバを含んでいても、オブジェクトの大きさは 4 でしかありません。この 4 は、静的でないデータメンバ m_value の分です。

静的データメンバは静的ストレージ期間を持っており、プログラムの開始から終了まで存在し続けます(本編解説)。オブジェクトが作られたり消えたりすることとはまったく無関係な存在というわけです。

問題3 (基本★★)

あるクラスのオブジェクトが作られるたびに、それぞれのオブジェクトに重複がない個別の ID (整数値) を割り当てたいとします。静的データメンバを利用して、このような割り当てを自動化できるようにクラスを作成してください。


重複がなければいいというだけの仕様ならば、0 から初めて連番を振っていくという実装で良いでしょう。次に割り当てる ID を覚えておく静的データメンバを用意すれば実現できます。

// MyClass.h
#ifndef MYCLASS_H_INCLUDED
#define MYCLASS_H_INCLUDED

class MyClass {
public:
    MyClass();

    inline int get_id() const
    {
        return m_id;
    }

private:
    int m_id;
    static int ms_next_id;
};

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

int MyClass::ms_next_id = 0;

MyClass::MyClass() : m_id {ms_next_id}
{
    ++ms_next_id;
}
// main.cpp
#include "MyClass.h"

int main()
{
    for (int i = 0; i < 5; ++i) {
        MyClass c {};
        std::cout << c.get_id() << "\n";
    }
}

実行結果:

0
1
2
3
4

コンストラクタで静的データメンバの値をインクリメントしています。

問題4 (応用★★)

生徒に関する情報を管理する Studentクラスを作成してください。ここでは情報として、生徒の氏名、3教科(国語、数学、英語)の得点を扱うものとします。得点をあつかう部分を入れ子クラスにして実現してみてください。


たとえば次のように作成できます。

// Student.cpp
#include "Student.h"
#include <algorithm>
#include <utility>

Student::Student(const std::string& name, int japanese, int math, int english) :
    m_name {name},
    m_score {japanese, math, english}
{}

Student::Score::Score(int japanese, int math, int english)
{
    m_values.resize(3);
    m_values[(int)Subject::japanese] = japanese;
    m_values[(int)Subject::math]     = math;
    m_values[(int)Subject::english]  = english;
}

int Student::Score::get_average() const
{
    int sum {0};
    for (auto score : m_values) {
        sum += score;
    }
    return sum / m_values.size();
}

std::pair<Student::Score::Subject, int> Student::Score::get_best_subject() const
{
    auto it = std::max_element(std::cbegin(m_values), std::cend(m_values));
    return { static_cast<Subject>(it - std::cbegin(m_values)), *it };
}

std::pair<Student::Score::Subject, int> Student::Score::get_worst_subject() const
{
    auto it = std::min_element(std::cbegin(m_values), std::cend(m_values));
    return { static_cast<Subject>(it - std::cbegin(m_values)), *it };
}
// Student.h
#ifndef STUDENT_H_INCLUDED
#define STUDENT_H_INCLUDED

#include <string>
#include <vector>

class Student {
public:

    // 成績
    class Score {
    public:

        // 得点データの型
        using values_t = std::vector<int>;

        // 科目
        enum class Subject {
            japanese,
            math,
            english,
        };

    public:
        Score(int japanese, int math, int english);

        // 得点データを返す
        inline const values_t* get_values() const
        {
            return &m_values;
        }
        
        // 平均点を返す
        int get_average() const;

        // 最も成績の良い科目と得点を返す
        std::pair<Subject, int> get_best_subject() const;

        // 最も成績の悪い科目と得点を返す
        std::pair<Subject, int> get_worst_subject() const;

    private:
        values_t m_values;
    };

public:
    Student(const std::string& name, int japanese, int math, int english);

public:
    inline const std::string& get_name() const
    {
        return m_name;
    }

    inline const Score& get_score() const
    {
        return m_score;
    }

private:
    std::string  m_name;
    Score        m_score;
};

#endif
// main.cpp
#include <iostream>
#include "Student.h"

namespace {

// 科目の文字列表現を返す
const char* get_subject_name(Student::Score::Subject subject)
{
    switch (subject) {
    case Student::Score::Subject::japanese:
        return "Japanese";
    case Student::Score::Subject::math:
        return "Math";
    case Student::Score::Subject::english:
        return "English";
    }
    return "???";
}

}   // end of namespace


int main()
{
    std::vector<Student> students {};
    students.push_back({"Tanaka Miki", 92, 66, 75});
    students.push_back({"Kouno Youhei", 65, 81, 72});
    students.push_back({"Ueda Yuki", 83, 77, 76});

    for (const Student& student : students) {
        const auto score = student.get_score();
        const auto best = score.get_best_subject();
        const auto worst = score.get_worst_subject();

        std::cout << "Name: " << student.get_name() << "\n"
                  << "  Average: " << score.get_average() << "\n"
                  << "  Best: " << get_subject_name(best.first) << " " << best.second << "\n"
                  << "  Worst: " << get_subject_name(worst.first) << " " << worst.second << "\n";
    }
}

実行結果:

Name: Tanaka Miki
  Average: 77
  Best: Japanese 92
  Worst: Math 66
Name: Kouno Youhei
  Average: 72
  Best: Math 81
  Worst: Japanese 65
Name: Ueda Yuki
  Average: 78
  Best: Japanese 83
  Worst: English 76

問題5 (応用★★★)

ペイントスクリプトのプログラムで使う、コマンドの実行を行う CommandExecutorクラスを作成してください。

ペイントスクリプトのコマンドは、たとえば次のような形式のものでした(「多次元配列」のページを参照)。

fill 255 255 255
pen 255 0 0
line 100 100 200 200
pen 0 0 255
line 200 100 100 200
save test.bmp

つまり、コマンド名と、それに応じたいくつかのパラメータが1セットです。CommandExecutorクラスは、このセットを1つ渡すと、その内容を解読して実行を行うクラスです。今回は、以下のコマンドを用意してください。

キャンバス側で必要な実装コードは、「クラス」のページの練習問題で作成しました。

コンストラクタに Canvas の参照を渡し、execメンバ関数にコマンドとパラメータが入った std::vector を渡すことでコマンドが実行されるものとします。つまり、以下のコードを含むようにしてください。これ以外の仕様は自由で構いません。

namespace paint_script {
    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);

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


とりあえずコンストラクタから片づけましょう。Canvasクラスの参照を渡してもらっていますが、これを実際に使うのは、各コマンドを実行するときになってからなので、データメンバとして保存しておく必要があります。

// command_executor.h
namespace paint_script {
    class CommandExecutor {
    public:
        // コンストラクタ
        //
        // canvas: キャンバスの参照
        explicit CommandExecutor(Canvas& canvas);

    private:
        Canvas&                 m_canvas;
    };
}
// command_executor.cpp

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

次に execメンバ関数の実装です。まず、問題に示されたコードから分かるとおり、コマンドとパラメータは CommandExecutor::command_params_t という型メンバ(本編解説)で表現します。この型は具体的には std::vector<std::string> ですから、コマンドもパラメータも std::string で表現する方針だということです。コマンド名が必ず先頭にあります。

// command_executor.cpp

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

        // コマンドを探す
        // 正しいコマンドが存在していたら、そのコマンドを実行する関数にパラメータを渡して呼び出す。
        // 正しいコマンドが存在していなかったら、エラーを表す戻り値を返す。
    }
}

コマンドとパラメータは、プログラムを実行した人が入力するものなので、間違っている可能性があります。正しいコマンドでなかったら、その旨を表す戻り値を返すようにします。戻り値の型は、CommandExecutor::ExecResult という scoped enum です。コマンドが見つからない場合は、CommandExecutor::ExecResult::not_found です。

受付可能なコマンドとは何なのか、そしてそのそれぞれで実行するべき処理(関数)は何であるのかを管理しなければなりません。コマンド名とメンバ関数ポインタ(「クラス」のページを参照)をまとめた構造体と、その配列を用意します。

// command_executor.h

namespace paint_script {
    class CommandExecutor {
    private:
        using CommandImpl_t = std::function<ExecResult (CommandExecutor*, const command_params_t&)>;    // 実装関数の型   

        // コマンドデータ
        struct CommandData {
            const char*     name;   // コマンド名
            CommandImpl_t   impl;   // コマンドの実装関数
        };
        static const CommandData CommandMap[];

    private:
        ExecResult exit(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);
        ExecResult resize(const command_params_t& cmd_vec);
    };
}
// command_executor.cpp

namespace paint_script {
    const CommandExecutor::CommandData CommandExecutor::CommandMap[] {
        {"exit", &exit},
        {"fill", &fill},
        {"pen", &pen},
        {"dot", &dot},
        {"rect", &rect},
        {"filled_rect", &filled_rect},
        {"resize", &resize},
        {"load", &load},
        {"save", &save},
    };
}

execメンバ関数に戻って、コマンドを探して、それぞれの関数を呼び出すコードを書きます。

// command_executor.cpp

namespace paint_script {
    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;
        }
        
        // 実行
        return command_it->impl(this, command_vec);
    }
}

各コマンドの実行関数は以下のように実装できます。まず exit は単純です。CommandExecutor::ExecResult型には、exit_program が定義されており、これが返されたらプログラムを終了するという仕様です。つまり、実際にプログラムを終了させる役割は CommandExecutorクラスではなく、execメンバ関数を呼び出した側に課せられています。

// command_executor.cpp

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

次に、キャンバスに描くタイプのコマンドです。キャンバスに何かを描く処理は Canvasクラス側に持たせるように作ってきているので、それを呼び出せばいいです。Canvasクラスの参照をコンストラクタで受け取っているので、呼び出すのは簡単です。なお、前にも書きましたが、コマンドとパラメータは人が入力するものなので必ず間違いが入ります。エラーのチェックはしっかりしましょう。

また、「現在の色」を管理しなければなりません。色は Penクラスを使って、「ペン」と見立てて管理するのでした。CommandExecutorクラスのデータメンバに Penクラスのオブジェクトを保存することにします。

// command_executor.h

namespace paint_script {
    class CommandExecutor {
    private:
        Pen                     m_pen {{0, 0, 0}};
    };
}
// command_executor.cpp

namespace paint_script {
    
    namespace {
        // std::string から int への変換
        int to_int(const std::string& s)
        {
            std::istringstream iss(s);
            int value {};
            iss >> value;
            if (!iss) {
                std::cout << s << "を整数に変換できません。\n";
                return 0;
            }
            return value;
        }
    }


    // キャンバスを塗りつぶす
    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 {to_int(cmd_vec.at(1))};
        if (red < 0 || 255 < red) {
            std::cout << "赤成分の強さは 0 から 255 の範囲でなければなりません。\n";
            print_help_fill();
            return ExecResult::failed;
        }

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

        const int blue {to_int(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::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 {to_int(cmd_vec.at(1))};
        const int y {to_int(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 {to_int(cmd_vec.at(1))};
        const int top {to_int(cmd_vec.at(2))};
        const int right {to_int(cmd_vec.at(3))};
        const int bottom {to_int(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 {to_int(cmd_vec.at(1))};
        const int top {to_int(cmd_vec.at(2))};
        const int right {to_int(cmd_vec.at(3))};
        const int bottom {to_int(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;
    }
}

間違った入力を検出したとき、それぞれのコマンドの正しい使い方を提示すると親切です。print_help_*** という名前の関数を呼んでいるところがそれです。これらの関数の実装はあとで取り上げます。

また、std::string から int に変換するために、to_int関数を自作しています。いつものように std::istringstream を使って変換しているだけですが、変換できない場合の対処には問題があります。変換に失敗する場合というのは、間違って abc のような文字列が入力されてきたとき、int に変換できないという状況です。この場合、コマンドの実行は行われないのが正しいといえますが、だいぶ面倒なコードになってくるため、ここではエラーメッセージを出力して、0 を返しておくことでごまかしています。

次は penコマンドです。「クラス」のページの練習問題で取り上げたとおり、Penクラスが保持する色はあとから変更できないようにする方針です。Penクラスの新しいオブジェクトを作り直します。

// command_executor.cpp

namespace paint_script {

    // ペンを変更する
    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 {to_int(cmd_vec.at(1))};
        if (red < 0 || 255 < red) {
            std::cout << "赤成分の強さは 0 から 255 の範囲でなければなりません。\n";
            print_help_pen();
            return ExecResult::failed;
        }

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

        const int blue {to_int(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;
    }
} 

次に resizeコマンドです。これはキャンバスの状態を変更するものなので、Canvasクラス側に実装を行う必要もあります。

// command_executor.cpp

namespace paint_script {

    // キャンバスの大きさを変更する
    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 {to_int(cmd_vec.at(1))};
        if (width < 1) {
            std::cout << "横方向のピクセル数は 1 以上でなければなりません。\n";
            print_help_resize();
            return ExecResult::failed;
        }

        const int height {to_int(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;
    }
}
// canvas.h

namespace paint_script {

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

}
// canvas.cpp

namespace paint_script {

    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);
    }
}

続いて、loadコマンドと saveコマンドです。キャンバスの内容が見えていないと実装できないので、これは Canvasクラスにメンバ関数を実装する必要もあります。ついでに Canvasクラスの全体像を載せます。

// command_executor.cpp

namespace paint_script {

    // ビットマップファイルから読み込む
    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;
    }
}
// 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
// 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 {width},
        m_height {height}
    {
        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;
    }

}

後回しにしていた、使い方を出力する関数を仕上げます。この手の重要なメッセージの出力は、最後に std::endl を付けて、確実にその場で出力されるようにしておきましょう(「ファイルとエラー」のページを参照)。

// command_executor.h

namespace paint_script {
    class CommandExecutor {
    private:
        void print_help_exit() 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_resize() const;
        void print_help_load() const;
        void print_help_save() const;
    };
}
// command_executor.cpp

namespace paint_script {

    void CommandExecutor::print_help_exit() const
    {
        std::cout << "exit\n"
                  << "スクリプトの実行を終了します。\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_resize() const
    {
        std::cout << "resize width height\n"
                  << "キャンバスの大きさを変更します。\n"
                  << "  width:  横方向のピクセル数を 1 以上の大きさで指定します。\n"
                  << "  height: 縦方向のピクセル数を 1 以上の大きさで指定します。\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;
    }
}

以上で、CommandExecutorクラスが使用できるところまで整備できました。使用側のイメージは次のようになります。

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;
        }
    }
}

標準入力から受け取る文字列は、fill 255 255 255 のような感じなので、これを std::vector<std::string> の形に直すために一手間必要です。このコードでは split関数という未実装関数を使っているのでコンパイルできません。この関数の実装は次のページで取り上げます。

現時点で動作確認を行うには、コマンドとパラメータの一覧を直接ソースファイルに書けばいいでしょう。

//main.cpp
#include "canvas.h"
#include "command_executor.h"

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

    std::vector<std::string> command_vecs[] {
        {"fill", "255", "255", "255"},
        {"pen", "255", "0", "0"},
        {"line", "100", "100", "200", "200"},
        {"pen", "0", "0", "255"},
        {"line", "200", "100", "100", "200"},
        {"save", "test.bmp"}
    };

    for (const auto& command_vec : command_vecs) {

        // 該当するコマンドを探して実行
        if (executor.exec(command_vec) == paint_script::CommandExecutor::ExecResult::exit_program) {
            break;
        }
    }
}

問題6 (発展★★★)

問題5で作ったプログラムに helpコマンドを追加実装してください。このコマンドは、存在するすべてのコマンドの一覧と、それぞれの使い方を出力します。

今後、コマンドがさらに追加されたときに、helpコマンドの解説に登場しないミスを防ぐことを考えて実装しましょう。


すでに各コマンドに対応したヘルプメッセージを出力する関数は作ってあるので、基本的にはそれを全部呼び出せばいいだけです。たとえば、次のように実装できます。helpコマンド自体のヘルプ出力関数も作っておきます。

// command_executor.cpp

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

        print_help_help();
        print_help_exit();
        print_help_fill();
        print_help_pen();
        print_help_dot();
        print_help_rect();
        print_help_filled_rect();
        print_help_resize();
        print_help_load();
        print_help_save();
        return ExecResult::successd;
    }

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

これでも良いのですが、新しいコマンドが追加されたときに忘れずに追加しなければならないことに、保守の面での問題が残ります。新しいコマンドを実装するときに、同時にヘルプ出力関数を実装することを強制し、かつ、helpコマンドに手を加える必要がないかたちが理想的です。

この理想は、CommandExecutor::CommandMap を拡張することで実現できます。

まず、CommandExecutor::CommandData構造体にヘルプ出力関数を追加します。

// command_executor.h

namespace paint_script {
    class CommandExecutor {
    private:
        using CommandHelp_t = std::function<void (const CommandExecutor*)>;                     // ヘルプ出力関数の型

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

CommandExecutor::CommandMap の中身にヘルプ出力関数を追加します。

// command_executor.cpp

namespace paint_script {
    const CommandExecutor::CommandData CommandExecutor::CommandMap[] {
        {"help", &help, &print_help_help},
        {"exit", &exit, &print_help_exit},
        {"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},
        {"resize", &resize, &print_help_resize},
        {"load", &load, &print_help_load},
        {"save", &save, &print_help_save},
    };
}

こうして一覧になっていれば、範囲for文で全体を走査しながらヘルプ出力関数を呼び出すことができます。1つ1つ書く必要がなくなるので、helpコマンドの実装を保守する必要性がなくなります。

// command_executor.cpp

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

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

実行結果:

help  <-- 入力した内容
以下のコマンドがあります。
対応するパラメータがある場合は、その順番どおりに、正しい値をスペースで区切って入力してください。

help
ヘルプメッセージを出力します。

exit
スクリプトの実行を終了します。

fill red green blue
キャンバスを1色で塗りつぶします。
  red:   赤成分の強さを 0 から 255 の範囲で指定します。
  green: 緑成分の強さを 0 から 255 の範囲で指定します。
  blue:  青成分の強さを 0 から 255 の範囲で指定します。

pen red green blue
点や線を描くときに使うペンを変更します。
  red:   赤成分の強さを 0 から 255 の範囲で指定します。
  green: 緑成分の強さを 0 から 255 の範囲で指定します。
  blue:  青成分の強さを 0 から 255 の範囲で指定します。

dot x y
現在のペンを使って、点を描画します。
  x: X座標を指定します。キャンバスの範囲外の場合は何も描かれません。
  y: Y座標を指定します。キャンバスの範囲外の場合は何も描かれません。

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

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

resize width height
キャンバスの大きさを変更します。
  width:  横方向のピクセル数を 1 以上の大きさで指定します。
  height: 縦方向のピクセル数を 1 以上の大きさで指定します。

load path
.bmpファイルを指定して、キャンバスを作成します。
  path: 読み込む .bmpファイルのパス。

save path
現在のキャンバスの状態を .bmp ファイルに保存します。
  path: 出力する .bmpファイルのパス。すでに存在する場合は上書きします。


参考リンク



更新履歴




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