web-dev-qa-db-ja.com

C ++例外クラスの設計

一連の例外クラスに適した設計とは何ですか?

私は例外クラスが何をすべきか、すべきでないかについてあらゆる種類のものを見ていますが、使いやすく拡張してそれらのことを行う単純なデザインではありません。

  1. 例外クラスは例外をスローすべきではありません。これは、エラーなどをログに記録する機会なしにプロセスを直接終了させる可能性があるためです。
  2. エラーから回復できない場合にアプリケーションが終了する前に、何かわかりやすいように、ユーザーフレンドリーな文字列を取得できるようにする必要があります。
  3. たとえば、XMLパーサーが入力ストリームの解析に失敗した場合や、ソースがファイルからのものであったこと、またはネットワーク経由であったことを追加できるようにするには、スタックが展開するときに情報を追加できる必要があります。
  4. 例外ハンドラーは、例外を処理するために必要な情報に簡単にアクセスできる必要があります。
  5. フォーマットされた例外情報をログファイルに書き込みます(英語なので、ここには翻訳しません)。

1と4を連携させることが私が抱えている最大の問題です。これは、フォーマットやファイルの出力メソッドが失敗する可能性があるためです。

編集:したがって、いくつかのクラスの例外クラスを見て、Neilがリンクされている質問でも、アイテム1(したがってブーストの推奨事項)を完全に無視するのが一般的な慣習のようですが、これはかなり悪い考えのようです私。

とにかく、使用したいと思っている例外クラスも投稿したいと思いました。

class Exception : public std::exception
{
    public:
        // Enum for each exception type, which can also be used
        // to determine the exception class, useful for logging
        // or other localisation methods for generating a
        // message of some sort.
        enum ExceptionType
        {
            // Shouldn't ever be thrown
            UNKNOWN_EXCEPTION = 0,

            // The same as above, but it has a string that
            // may provide some information
            UNKNOWN_EXCEPTION_STR,

            // For example, file not found
            FILE_OPEN_ERROR,

            // Lexical cast type error
            TYPE_PARSE_ERROR,

            // NOTE: in many cases functions only check and
            //       throw this in debug
            INVALID_ARG,

            // An error occured while trying to parse
            // data from a file
            FILE_PARSE_ERROR,
        }

        virtual ExceptionType getExceptionType()const throw()
        {
            return UNKNOWN_EXCEPTION;
        }

        virtual const char* what()throw(){return "UNKNOWN_EXCEPTION";}
};


class FileOpenError : public Exception
{
    public:
        enum Reason
        {
            FILE_NOT_FOUND,
            LOCKED,
            DOES_NOT_EXIST,
            ACCESS_DENIED
        };
        FileOpenError(Reason reason, const char *file, const char *dir)throw();
        Reason getReason()const throw();
        const char* getFile()const throw();
        const char* getDir ()const throw();

    private:
        Reason reason;
        static const unsigned FILE_LEN = 256;
        static const unsigned DIR_LEN  = 256;
        char file[FILE_LEN], dir[DIR_LEN];
};

すべての文字列は内部の固定サイズのバッファーにコピーすることで処理されるため、ポイント1が処理されます(必要に応じて切り捨てますが、常にnullで終了します)。

これはポイント3を扱っていませんが、とにかく現実の世界での使用は限定的である可能性が高く、必要に応じて新しい例外をスローすることで対応できる可能性が高いと思います。

42
Fire Lancer

例外クラスの浅い階層を使用します。階層を深くしすぎると、値よりも複雑になります。

例外クラスをstd :: exception(またはstd :: runtime_errorなどの他の標準例外の1つ)から派生させます。これにより、最上位の汎用例外ハンドラーが、処理しない例外を処理できるようになります。たとえば、エラーをログに記録する例外ハンドラーがある場合があります。

これが特定のライブラリまたはモジュールに関するものである場合は、モジュールに固有のベースが必要な場合があります(標準の例外クラスの1つからまだ派生しています)。呼び出し元は、この方法でモジュールから何かをキャッチすることを決定する場合があります。

あまり多くの例外クラスを作成しません。例外に関する多くの詳細をクラスにパックできるため、必ずしもエラーの種類ごとに一意の例外クラスを作成する必要はありません。一方、処理する予定のエラーには一意のクラスが必要です。パーサーを作成している場合、さまざまなタイプの構文エラーに特化したものではなく、問題の詳細を説明するメンバーを持つ単一のsyntax_error例外が発生する可能性があります。

例外の文字列はデバッグ用です。ユーザーインターフェイスでは使用しないでください。他の言語への翻訳などを可能にするために、UIとロジックをできるだけ分離する必要があります。

例外クラスには、問題の詳細を示す追加のフィールドを含めることができます。たとえば、syntax_error例外には、ソースファイル名、行番号などが含まれる可能性があります。例外を作成またはコピーして別の例外をトリガーする可能性を減らすために、これらのフィールドの基本タイプにできるだけ従います。たとえば、例外にファイル名を格納する必要がある場合、std :: stringではなく、固定長のプレーン文字配列が必要になることがあります。 std :: exceptionの一般的な実装では、mallocを使用して理由文字列を動的に割り当てます。 mallocが失敗した場合、ネストされた例外をスローしたりクラッシュしたりするのではなく、理由文字列を犠牲にします。

C++の例外は、「例外的な」条件である必要があります。したがって、解析例は良いものではないかもしれません。ファイルの解析中に発生した構文エラーは、例外によって処理されることを保証するほど特別なものではない可能性があります。条件が明示的に処理されない限りプログラムが続行できない場合、何かが例外的だと思います。したがって、ほとんどのメモリ割り当てエラーは例外的ですが、ユーザーからの不適切な入力はおそらく例外ではありません。

24
Adrian McCarthy

仮想継承を使用します。この洞察は、Andrew Koenigによるものです。例外の基本クラスからの仮想継承を使用すると、共通の基本クラスを持つ複数の基本から派生した例外が誰かによってスローされた場合に、キャッチサイトでのあいまいさの問題を回避できます。

ブーストサイト に関する他の同様に役立つアドバイス

7
jon-hanson


2:いいえ、ユーザーインターフェイス(=ローカライズされたメッセージ)とプログラムロジックを混在させることはできません。アプリケーションが問題を処理できないことをアプリケーションが認識した場合、ユーザーへの通信は外部レベルで行う必要があります。例外の情報のほとんどは、実装の詳細が多すぎてユーザーに表示できません。
3:これにはboost.exceptionを使用します
5:いいえ、これを行わないでください。 2を参照してください。ログを記録するかどうかの決定は、常にエラー処理サイトで行う必要があります。

1種類の例外のみを使用しないでください。アプリケーションが必要なエラー回復のタイプごとに個別のキャッチハンドラーを使用できるように、十分なタイプを使用してください

7
grimner

例外クラス階層の設計に直接関係はありませんが、重要な(そしてそれらの例外の使用に関連する)重要なことは、一般に 値でスローし、参照でキャッチする必要があるということです

これにより、スローされた例外のメモリの管理に関連する問題(ポインタをスローした場合)およびオブジェクトのスライスの可能性(例外を値でキャッチした場合)が回避されます。

4
Michael Burr

以降 std::nested_exception および std::throw_with_nestedC++ 11で利用可能になりました。StackOverflowで回答を示したいと思います here および ここ

これらの答えは、適切な例外ハンドラーを作成するだけで、デバッガーや面倒なロギングを必要とせずに、コード内で例外のバックトレースを取得する方法を説明します。ネストされた例外を再スローします。

私の意見では、例外の設計では、例外クラス階層を作成せず、ライブラリごとに1つの例外クラスのみを作成することも提案しています(すでに指摘したとおり)。アウト この質問への回答 )。

3
GPMueller

優れた設計は、例外クラスのセットを作成することではありません。std:: exceptionに基づいて、ライブラリごとに1つだけ作成してください。

情報の追加はかなり簡単です:

try {
  ...
}
catch( const MyEx & ex ) {
   throw MyEx( ex.what() + " more local info here" );
}

また、例外ハンドラーは例外ハンドラーであるため、必要な情報を持っています。tryブロックの関数のみが例外を発生させることができるため、ハンドラーはこれらのエラーを考慮するだけで済みます。また、一般的なエラー処理に例外を使用すべきではありません。

基本的に、例外はできる限り単純でなければなりません-直接接続されるべきではないログファイルのようです。

これは以前に尋ねられたと思いますが、今は見つかりません。

2
anon