‼️

CPP-MODULE05 : 繰り返し、例外処理

2024/11/16に公開

この問題の意図は、C++におけるクラス設計の基本を学びながら、例外処理オーバーロードといった、オブジェクト指向プログラミング(OOP)の重要な概念を理解することです。特に重要なのは、クラスごとの責任分担と、エラーが発生した場合に安全に処理できるようにする例外処理の実装です。この課題を通して、オブジェクト指向プログラミングの基礎をしっかりと理解し、現実のシステムにおける設計手法を学ぶことができます。具体的な意図を解説し、重要なポイントを挙げていきます。

問題の意図

  1. クラス設計の基本を理解させる

    • Bureaucrat(官僚)とForm(書類)という2つのクラスを作成することで、クラス間の関係性や役割分担を考慮した設計を学びます。各クラスは、それぞれが持つべき責任(プロパティやメソッド)を持つことで、コードが分かりやすく、保守性が高いものになります。
  2. 例外処理の重要性を理解させる

    • クラスのインスタンス化やメソッド呼び出し時に、グレードの範囲を守るために例外処理を導入しています。これにより、エラーが発生した場合でもプログラムが安全に実行されるように設計されています。
    • 例外クラスの実装により、カスタムの例外を投げる方法や、エラーメッセージを管理する方法を学びます。
  3. オーバーロードの概念を学ばせる

    • << 演算子のオーバーロードにより、オブジェクトの状態を簡単に表示できるようにすることで、演算子オーバーロードのメリットを学びます。これにより、出力のカスタマイズが可能となり、可読性の向上が期待されます。
  4. クラス間の相互作用を設計させる

    • Bureaucrat クラスが Form クラスを操作する(例: signForm() メソッドを使ってサインする)ことで、クラス間の相互作用についての理解を深めます。これにより、クラスが単に独立した存在ではなく、他のクラスと協調して動作することを学びます。
  5. オブジェクトの状態を正しく管理する方法を学ばせる

    • フォームが「サインされた」かどうかという状態(is_signed)や、官僚のグレード(grade)が適切に管理されているかという、オブジェクトの状態管理を学びます。このような状態の変化を追跡することで、オブジェクト指向設計の基本的な考え方を理解します。

重要なポイント

  1. クラスの責任の分離

    • Bureaucrat(官僚)がグレードを持ち、Form(書類)がサインされる対象となるという、明確な責任分担を行う設計が求められています。各クラスは、役割に応じた適切な機能を提供します。
  2. 例外処理の実装

    • グレードが範囲外の場合に例外を発生させ、問題が発生したときに安全に処理できるように設計されています。この例外処理は、信頼性の高いプログラムを作る上で非常に重要なポイントです。
  3. カプセル化とアクセス制御

    • Form クラスのプロパティ(namegrade_to_signgrade_to_execute など)はprivateで定義されています。これにより、外部から直接これらのプロパティにアクセスできないようにし、クラスが内部の状態を安全に管理できるようにしています。
    • プロパティにアクセスするためのgetterメソッドを提供することで、データの安全性を保ちながら必要な情報を取得できます。
  4. オーバーロード

    • << 演算子のオーバーロードにより、BureaucratForm クラスの状態を簡単に出力できるようにします。これにより、クラスのオブジェクトの可視化が容易になり、デバッグや結果確認がしやすくなります。
  5. クラス間の関係

    • BureaucratForm に対してサインを試みることで、クラス間の協力を表現しています。このようなクラス間の相互作用は、実際のアプリケーション開発において非常に重要です。
  6. 例外とエラーメッセージの管理

    • 例外を投げる際に、適切なエラーメッセージを出力することで、エラーの原因を正確に特定し、問題解決に役立てることができます。

Exercise 00: 「官僚システムを作る」プログラミング解説

C++ Moduleでは、オブジェクト指向プログラミングの基礎を学ぶために、さまざまなクラスや例外処理の仕組みを利用して複雑なシステムを設計します。今回のエクササイズは、その中でも「官僚(Bureaucrat)」を題材にして、シンプルながらも重要なクラス設計と例外処理の基礎を学ぶことができる課題です。

このエクササイズは「Bureaucrat(官僚)」というクラスを作成することが目標です。クラスの設計から例外処理、出力のオーバーロードまで、重要なC++の機能を学んでいきます。では、初心者にも分かりやすく、この課題を順を追って説明していきましょう。

課題概要

このエクササイズでは、官僚(Bureaucrat)というクラスを実装します。以下が主な要件です。

  • 名前(name): 官僚の名前は不変(定数)です。
  • グレード(grade): 1が最高、150が最低のグレードです。
  • 官僚のグレードが範囲外の場合、例外が発生します(GradeTooHighExceptionまたはGradeTooLowException)。

基本的な要件

  • 官僚クラスには、名前とグレードを取得するためのゲッター(getName()getGrade())を用意します。
  • グレードの増減操作を行うメンバ関数(incrementGrade()decrementGrade())を実装し、範囲外の操作には例外を投げます。
  • グレードが範囲外(1より高く、150より低い)になると、例外をスローします。
  • 例外処理は trycatch ブロックを使ってキャッチします。
  • 出力オペレーター(<<)をオーバーロードして、<name>, bureaucrat grade <grade> の形式で官僚の情報を出力できるようにします。

では、各ステップを詳しく見ていきましょう。


1. Bureaucratクラスの定義

まず、Bureaucrat クラスのヘッダーファイルを定義します。このクラスには、名前とグレードを保持するためのメンバ変数、例外クラス、そしていくつかのメンバ関数が必要です。

#ifndef BUREAUCRAT_HPP
#define BUREAUCRAT_HPP

#include <iostream>
#include <stdexcept>
#include <string>

class Bureaucrat {
public:
    Bureaucrat(const std::string& name, int grade);
    ~Bureaucrat();

    const std::string& getName() const;
    int getGrade() const;

    void incrementGrade();
    void decrementGrade();

    class GradeTooHighException : public std::exception {
    public:
        const char* what() const throw();
    };

    class GradeTooLowException : public std::exception {
    public:
        const char* what() const throw();
    };

private:
    const std::string name_;
    int grade_;
};

std::ostream& operator<<(std::ostream& os, const Bureaucrat& bureaucrat);

#endif

2. Bureaucratクラスの実装

次に、Bureaucrat クラスの実装部分を作成します。

コンストラクタとメンバ関数

  • コンストラクタでは、名前とグレードを初期化し、グレードが範囲外の場合に例外をスローします。
  • incrementGrade() と decrementGrade() で、グレードの増減を行います。
#include "Bureaucrat.hpp"

Bureaucrat::Bureaucrat(const std::string& name, int grade) : name_(name), grade_(grade) {
    if (grade < 1) {
        throw GradeTooHighException();
    } else if (grade > 150) {
        throw GradeTooLowException();
    }
}

Bureaucrat::~Bureaucrat() {}

const std::string& Bureaucrat::getName() const {
    return name_;
}

int Bureaucrat::getGrade() const {
    return grade_;
}

void Bureaucrat::incrementGrade() {
    if (grade_ - 1 < 1) {
        throw GradeTooHighException();
    }
    grade_--;
}

void Bureaucrat::decrementGrade() {
    if (grade_ + 1 > 150) {
        throw GradeTooLowException();
    }
    grade_++;
}

例外クラス

  • GradeTooHighExceptionGradeTooLowException の実装です。それぞれの what() 関数が、エラーメッセージを返します。
const char* Bureaucrat::GradeTooHighException::what() const throw() {
    return "Grade is too high!";
}

const char* Bureaucrat::GradeTooLowException::what() const throw() {
    return "Grade is too low!";
}

3. 出力オペレータのオーバーロード

次に、Bureaucratオブジェクトを簡単に出力できるように、<<演算子をオーバーロードします。

std::ostream& operator<<(std::ostream& os, const Bureaucrat& bureaucrat) {
    os << bureaucrat.getName() << ", bureaucrat grade " << bureaucrat.getGrade();
    return os;
}

4. 例外処理とテスト

Bureaucratのインスタンスを作成し、例外が発生した場合にキャッチして処理します。例外が発生した場合、適切なメッセージを表示することで、エラーに対処します。

#include "Bureaucrat.hpp"

int main() {
    try {
        Bureaucrat bob("Bob", 2);
        std::cout << bob << std::endl;

        bob.incrementGrade();
        std::cout << bob << std::endl;

        bob.incrementGrade(); // これでGradeTooHighExceptionが発生
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }

    try {
        Bureaucrat alice("Alice", 149);
        std::cout << alice << std::endl;

        alice.decrementGrade();
        std::cout << alice << std::endl;

        alice.decrementGrade(); // これでGradeTooLowExceptionが発生
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}


まとめ

このエクササイズでは、Bureaucratクラスの作成を通じて、C++の基本的なクラス設計、例外処理、出力オペレーターのオーバーロードなどを学ぶことができる。特に例外処理は、エラーハンドリングを柔軟に行うために非常に重要であり、この課題を通じてその使い方を習得することができる。

Exercise 01: 「Form up, maggots!」プログラミング解説

このエクササイズでは、前の課題(Bureaucratクラス)を発展させ、新たに「Form(フォーム)」クラスを作成します。官僚(Bureaucrat)がフォームにサインするための一連の操作を実装し、例外処理やクラス設計をさらに学びます。今回の目標は、Formクラスを作り、官僚がフォームにサインできるようにすることです。

課題概要

  • Formクラスを作成し、以下の属性を持つ。
    • 名前(name): 定数(変更不可)。
    • サインの状態(is_signed): フォームがサインされているかを示すブール値。初期状態は「サインされていない」。
    • サインに必要なグレード(grade_to_sign): サインするために必要なグレード。
    • 実行に必要なグレード(grade_to_execute): フォームを実行するために必要なグレード。

これらのグレードは、Bureaucratクラスと同様に1〜150の範囲内で管理され、範囲外のグレードには例外が発生します。これに加え、フォームをサインできるかどうかを管理するbeSigned()メンバ関数と、官僚がフォームにサインを試みるsignForm()メンバ関数を実装します。


1. Formクラスの定義

まずは、Formクラスのヘッダーファイルを定義します。Bureaucratクラスとの関連性が重要なため、適切な関数や例外クラスを追加する必要があります。

Form.hppの作成

#ifndef FORM_HPP
#define FORM_HPP

#include <iostream>
#include <string>
#include "Bureaucrat.hpp"

class Form {
public:
    // コンストラクタとデストラクタ
    Form(const std::string& name, int grade_to_sign, int grade_to_execute);
    ~Form();

    // ゲッター関数
    const std::string& getName() const;
    bool isSigned() const;
    int getGradeToSign() const;
    int getGradeToExecute() const;

    // メンバ関数
    void beSigned(const Bureaucrat& bureaucrat);

    // 例外クラス
    class GradeTooHighException : public std::exception {
    public:
        const char* what() const throw();
    };

    class GradeTooLowException : public std::exception {
    public:
        const char* what() const throw();
    };

private:
    const std::string name_;
    bool is_signed_;
    const int grade_to_sign_;
    const int grade_to_execute_;
};

// 出力オペレータのオーバーロード
std::ostream& operator<<(std::ostream& os, const Form& form);

#endif

2. Formクラスの実装

次に、Formクラスの実装を行います。

3. Bureaucratクラスの更新

次に、Bureaucratクラスにフォームにサインを試みる機能を追加します。具体的には、signForm()メンバ関数を追加して、官僚がフォームにサインできるかどうかを判断し、結果を出力します。

Bureaucrat.cppの更新

#include "Bureaucrat.hpp"
#include "Form.hpp"

void Bureaucrat::signForm(Form& form) const {
    try {
        form.beSigned(*this);
        std::cout << getName() << " signed " << form.getName() << std::endl;
    } catch (const std::exception& e) {
        std::cout << getName() << " couldn't sign " << form.getName() << " because " << e.what() << std::endl;
    }
}

4. テスト

実装が完了したら、テストを行って動作を確認します。フォームを作成し、Bureaucratがそのフォームにサインできるかどうかをテストします。


5.抽象クラスについて

「抽象クラス」とは、少なくとも1つの純粋仮想関数を持つクラスのことです。純粋仮想関数を持つクラスは、そのままでは未完成とみなされ、直接インスタンス化することができません。抽象クラスは継承されることを前提として設計されており、派生クラスで純粋仮想関数を具体的に実装する必要があります。

まとめ

このエクササイズでは、官僚(Bureaucrat)がフォーム(Form)にサインするというシステムを実装した。グレードのチェックを通じて、フォームが正しくサインされるかどうかを管理し、例外処理を通じてエラーを適切に処理することを学んだ。サインできない場合には例外が投げられ、それをtry / catchでキャッチして処理するという流れが重要である。

Exercise 02: 「No, you need form 28B, not 28C...」プログラミング解説

C++ ModuleのExercise 02では、前回のFormクラスを発展させ、さらに実際に何かアクションを行う具体的なフォームクラスを実装する。この課題では、Formクラスを抽象クラス(AForm)に変換し、さらに3つの具体的なフォーム(ShrubberyCreationFormRobotomyRequestFormPresidentialPardonForm)を作成することが目的である。

このエクササイズを通して、C++の継承抽象クラス例外処理ポリモーフィズムといった重要なオブジェクト指向の概念を実践することができる。

課題概要

  • AFormという抽象クラスを作成し、そこから派生する具体的なフォームを3つ作る。
  • それぞれのフォームは、サインに必要なグレード実行に必要なグレードが異なる。
  • 各フォームは特定のターゲットに対して行動を起こす。
  • フォームがサインされていない場合や、官僚のグレードが低すぎる場合には例外が発生する。

具体的なフォームクラス

  1. ShrubberyCreationForm(低木を作成するフォーム)

    • サインに必要なグレード:145
    • 実行に必要なグレード:137
    • 指定したターゲットにASCIIアートの木を描く。
  2. RobotomyRequestForm(ロボトミー(脳手術)を要求するフォーム)

    • サインに必要なグレード:72
    • 実行に必要なグレード:45
    • ターゲットを50%の確率でロボトミー成功とし、失敗時にはその旨を通知する。
  3. PresidentialPardonForm(大統領の恩赦を要求するフォーム)

    • サインに必要なグレード:25
    • 実行に必要なグレード:5
    • ターゲットがZaphod Beeblebroxにより恩赦される。

1. AForm(抽象クラス)の定義と実装

まず、Formクラスを抽象クラス(AForm)に変更する。これにより、具体的なフォームクラスが継承して使えるようにする。

AForm.hpp の定義

#ifndef AFORM_HPP
#define AFORM_HPP

#include <iostream>
#include <stdexcept>
#include <string>
#include "Bureaucrat.hpp"

class AForm {
public:
    AForm(const std::string& name, int grade_to_sign, int grade_to_execute);
    virtual ~AForm();

    const std::string& getName() const;
    bool isSigned() const;
    int getGradeToSign() const;
    int getGradeToExecute() const;
    void beSigned(const Bureaucrat& bureaucrat);

    virtual void execute(Bureaucrat const & executor) const = 0;

    class GradeTooHighException : public std::exception {
    public:
        const char* what() const throw();
    };

    class GradeTooLowException : public std::exception {
    public:
        const char* what() const throw();
    };

    class FormNotSignedException : public std::exception {
    public:
        const char* what() const throw();
    };

private:
    const std::string name_;
    bool is_signed_;
    const int grade_to_sign_;
    const int grade_to_execute_;
};

// 出力オペレータのオーバーロード
std::ostream& operator<<(std::ostream& os, const AForm& form);

#endif

2. ShrubberyCreationFormの実装

ShrubberyCreationFormは、指定されたターゲットに低木を描くフォームである。

ShrubberyCreationForm.hpp

#ifndef SHRUBBERYCREATIONFORM_HPP
#define SHRUBBERYCREATIONFORM_HPP

#include "AForm.hpp"
#include <fstream>

class ShrubberyCreationForm : public AForm {
public:
    ShrubberyCreationForm(const std::string& target);
    virtual ~ShrubberyCreationForm();

    void execute(Bureaucrat const & executor) const;

private:
    const std::string target_;
};

#endif

ShrubberyCreationForm.cpp

#include "ShrubberyCreationForm.hpp"

ShrubberyCreationForm::ShrubberyCreationForm(const std::string& target)
    : AForm("Shrubbery Creation", 145, 137), target_(target) {}

ShrubberyCreationForm::~ShrubberyCreationForm() {}

void ShrubberyCreationForm::execute(Bureaucrat const & executor) const {
    if (!isSigned()) {
        throw AForm::FormNotSignedException();
    }
    if (executor.getGrade() > getGradeToExecute()) {
        throw AForm::GradeTooLowException();
    }

    std::ofstream file(target_ + "_shrubbery");
    if (file.is_open()) {
        file << "ASCII Tree" << std::endl;
        file.close();
    }
}

3. RobotomyRequestFormの実装

RobotomyRequestFormは、50%の確率でターゲットをロボトミーするフォームである。

RobotomyRequestForm.hpp

#ifndef ROBOTOMYREQUESTFORM_HPP
#define ROBOTOMYREQUESTFORM_HPP

#include "AForm.hpp"
#include <cstdlib>

class RobotomyRequestForm : public AForm {
public:
    RobotomyRequestForm(const std::string& target);
    virtual ~RobotomyRequestForm();

    void execute(Bureaucrat const & executor) const;

private:
    const std::string target_;
};

#endif

RobotomyRequestForm.cpp

#include "RobotomyRequestForm.hpp"
#include <iostream>

RobotomyRequestForm::RobotomyRequestForm(const std::string& target)
    : AForm("Robotomy Request", 72, 45), target_(target) {}

RobotomyRequestForm::~RobotomyRequestForm() {}

void RobotomyRequestForm::execute(Bureaucrat const & executor) const {
    if (!isSigned()) {
        throw AForm::FormNotSignedException();
    }
    if (executor.getGrade() > getGradeToExecute()) {
        throw AForm::GradeTooLowException();
    }

    std::cout << "Drilling noises..." << std::endl;
    if (rand() % 2 == 0) {
        std::cout << target_ << " has been robotomized successfully!" << std::endl;
    } else {
        std::cout << "Robotomy failed on " << target_ << "!" << std::endl;
    }
}

4. PresidentialPardonFormの実装

PresidentialPardonFormは、ターゲットが大統領に恩赦されるフォームである。

PresidentialPardonForm.hpp



#ifndef PRESIDENTIALPARDONFORM_HPP
#define PRESIDENTIALPARDONFORM_HPP

#include "AForm.hpp"

class PresidentialPardonForm : public AForm {
public:
    PresidentialPardonForm(const std::string& target);
    virtual ~PresidentialPardonForm();

    void execute(Bureaucrat const & executor) const;

private:
    const std::string target_;
};

#endif

5. Bureaucratクラスの更新

Bureaucratクラスには、フォームを実行するexecuteForm()メンバ関数を追加する。

Bureaucrat.cpp の更新

#include "Bureaucrat.hpp"
#include "AForm.hpp"

void Bureaucrat::executeForm(AForm const & form) const {
    try {
        form.execute(*this);
        std::cout << getName() << " executed " << form.getName() << std::endl;
    } catch (const std::exception& e) {
        std::cout << getName() << " couldn't execute " << form.getName() << " because " << e.what() << std::endl;
    }
}

まとめ

この課題では、C++の抽象クラス継承ポリモーフィズムを用いて、具体的なフォームクラスを実装し、各クラスが異なるアクションを実行するシステムを構築する。さらに、官僚がフォームをサインし、実行する流れを学び、オブジェクト間の相互作用や、例外処理を駆使して堅牢なプログラムを設計できるようになる。

重要なポイントは以下の通りである:

  • 抽象クラスを使った汎用的な設計。
  • 継承を利用して異なるフォームごとの動作を実装。
  • 例外処理によるエラーハンドリング。

Exercise 03: At least this beats coffee-making


この課題では、C++を使って「Intern(インターン)」というクラスと、異なる種類の「フォーム」を生成・操作するプログラムを実装します。このプロジェクトは、オブジェクト指向の基本であるクラスの継承や多態性(ポリモーフィズム)を学ぶのに適しており、特に動的メモリ管理例外処理の理解を深める練習となります。

課題の概要

このプロジェクトでは、「Intern」クラスと、いくつかの「フォーム」を表現するクラスを実装します。Internは社内で事務作業の補助を行うインターンの役割を担い、名前や階級などの特徴は持ちませんが、フォームを生成する「makeForm()」というメソッドを持ちます。課題では、以下の機能を実装することが求められます。

  1. Internクラスの実装。
  2. makeForm()メソッドで、特定の名前に対応するフォームオブジェクトを生成。
  3. 各フォームの生成と動作確認。
  4. 存在しないフォーム名を入力した際にエラーメッセージを出力。
  5. if-else構文を使わない方法でのコマンド管理(工夫した設計)。

この課題を通じて、基本的なC++のオブジェクト指向の概念を体系的に理解することができます。


実装の流れ

1. クラスの設計

まず、フォームを表す抽象クラスFormを設計し、それを継承する具体的なフォーム(例えばRobotomyRequestFormShrubberyCreationFormPresidentialPardonForm)を定義します。

  • Formクラス: フォームの共通機能(メソッドや属性)を定義します。例えば、フォームの名前、ターゲット、署名や実行の機能を持たせる設計です。さらに、ポリモーフィズムを実現するために、純粋仮想関数executeを定義します。
  • 派生クラス: Formを基底クラスとし、具体的な処理を持つ派生クラスを作成します。例えば、RobotomyRequestFormならターゲットの「ロボトミー処理」、PresidentialPardonFormなら「大統領による恩赦」などの特定の処理を実装します。

2. InternクラスとmakeFormメソッドの実装

Internクラスでは、makeForm()メソッドを使ってフォームの生成を行います。ここでのポイントは、フォーム名と対応するフォーム生成処理を動的に管理する点です。フォーム名と生成関数を対応させることで、複数の条件分岐(if-else文)を使わずにフォームの種類を動的に管理できるようにします。これは、関数ポインタやマップ(std::map)を使うことで実現します。

3. デストラクタとメモリ管理

フォームの生成には動的メモリ(new)を使用するため、使用後には必ずメモリを解放することが重要です。Formクラスには仮想デストラクタを定義し、派生クラスをForm*ポインタで扱った際に、適切なクラスのデストラクタが呼び出されるようにします。これにより、メモリリークや予期しない動作を防げます。

4. 例外処理の実装

存在しないフォーム名が指定された場合に例外をスローし、エラーメッセージを表示するようにします。例外を使用することで、エラーが発生してもプログラムが中断せず、適切なエラーメッセージを出力した上で処理を続行することができます。


ポイント

1. 仮想関数と純粋仮想関数

  • C++のオブジェクト指向の基礎となる概念で、基底クラスで宣言された純粋仮想関数は派生クラスで実装されなければならず、ポリモーフィズムを実現します。
  • 例: Formクラスのexecute()が純粋仮想関数であるため、派生クラスでexecute()を実装する必要があります。

2. 関数ポインタや関数オブジェクト

  • 関数ポインタや関数オブジェクトを使用することで、関数を動的に呼び出す仕組みを実装できます。これにより、大量のif-elseswitch-caseを避けることができます。

3. 仮想デストラクタ

  • 基底クラスに仮想デストラクタを定義することで、ポインタを通して動的に生成されたオブジェクトを削除したときに、派生クラスのデストラクタも正しく呼び出されるようになります。これにより、メモリリークを防ぎます。

4. 例外処理

  • try-catchブロックを使って、エラーが発生した際に適切に対処する方法を学びます。存在しないフォームが指定された場合や、権限が不足している場合のエラーメッセージ表示に使えます。

5. 動的メモリ管理

  • C++でnewを使ってオブジェクトを生成した場合、手動でdeleteを呼んでメモリを解放する必要があります。適切に解放しないと、メモリリークの原因になります。

まとめ

この課題を通じて、オブジェクト指向の基礎となる概念やC++の多態性、動的メモリ管理、例外処理について学べます。実際にインターンがフォームを生成して署名を集めたり、エラーがあれば適切に処理するという流れを通して、現実的なプログラムの設計や実装ができるようになります。この課題に取り組むことで、より理解が深まり、より柔軟で拡張性の高いコードを書く力が身につきます。

Discussion