C++の別のエラー処理ポリシー

13595 ワード

この短文は、多くのプログラマーが興味を持っている話題であるエラー処理について議論します.エラー処理はプログラミングの「暗い面」です.アプリケーションの「現実世界」の鍵であり、隠したい複雑なビジネスでもあります.
初期のCプログラミング生涯では、3つのエラー処理の方法を知っていました.
C言語の方式:エラーコードを返す
C言語スタイルのエラー処理は最も簡単ですが、完璧ではありません.
C言語スタイルのエラー処理は「プログラムがエラーに遭遇したときにエラーコードを返す」ことに依存する.ここでは簡単な例です.
int find_slash(const char *str)
{
    int i = 0;

    while (str[i] && str[i] != '/')
          i++;

    if (str[i] == '\0')
        return -1; //Error code

    //True value
    return i;
}

// . . .

if (find_slash(string) == -1)
{
        //error handling
}

この方法を使うメリットは何ですか?
関数を呼び出した後、エラーコードを直接処理したり(C言語でもそう処理します)、エラーメッセージを表示したり、プログラムを直接終了したりすることができます.あるいはプログラムの最近の状態だけを復元し、計算を終了します.
エラー処理がどこにあるか見つからない場合は、関数呼び出しを後で見るだけで、エラー処理はその近くにあります.
この方法を使うのは何が悪いですか.
このような異常/エラー処理方式と「実行ロジック」が混在していることを教えてくれる人もいるかもしれません.これらのコードを順番に読むと、プログラムが実行されるのと同じように、エラー処理が発生したり、プログラムが実行されたりします.これは大変です.読み取り専用プログラムの実行ロジックやエラー処理ロジックが好きかもしれません.
エラーコードの使用を制限されています.より多くの情報を提供するには、errstrやグローバル変数などの機能関数を作成する必要があります.
C++の使い方
C++はCの強化として,新しいエラー処理方式である異常を導入した.例外は、エラーを投げ出すことによって、通常のコード実行ロジックを中断し、他の場所でキャプチャすることができる.簡単な例を次に示します.
int find_slash(const char *str)
{
    int i = 0;

    while (str[i] && str[i] != '/')
          i++;

    if (str[i] == '\0')
        throw AnException("Error message");

    //True value
    return i;
}

// . . .

try
{
    find_slash(string);
}
catch(AnException& e)
{
   //Handle exception
}

このようにするメリットは?
プログラムロジックとエラー処理が分離された.関数がどのように動作しているかを見ることができますが、関数が失敗したときにどのように処理されているかを見ることができます.これは完璧で、エラー処理と正常なプログラムロジックを簡単に見ることができます.
また、カスタム例外オブジェクトに必要なコンテンツを埋め込むことができるため、エラーに対してできるだけ多くの情報を提供することができます.
こんな悪いところ
詳細な異常処理の作成が冗長になる.異常ツリーが必要ですが、あまり大きくしないほうがいいです.そうすれば、興味のある異常をキャプチャすることができます.また、内部では、エラーコードを提供して、何が起こったのかを知るとともに、エラーメッセージを検索する必要があります.書き込み異常クラスは通常冗長であり、情報をエラーに埋め込んでより多くの情報を柔軟に処理するコストである.
ここでのエラー処理哲学は、エラーをできるだけ処理する必要がある場所に延期して処理することです.プログラム実行プロセスがどこでエラーが発生するか分からない場合は、異なるファイルと機能関数をスキップして検索する必要があります.これは通常困難です.深い呼び出しツリー(関数呼び出しをグラフに描画し、ツリーのような形をしていることを意味する)で異常を起こした場合は、この異常をどこで処理するか、処理されたときにどこで発生するかを指定する必要があります.特に、あなたのプログラムが大きくて、前から書いていて、ちょうど設計が不十分なときは、もっと難しいように見えます.ほとんどのビジネスプロジェクトはそうです
だから私は「異常は危険だ」と思います.エラーを処理するのに良い方法を提供していますが、いくつかの小さなプロジェクトに限られ、ここでの呼び出し図は簡単で把握しやすいです.
エラーパッケージのモード
私はそれをモデルと呼んでいるので、人々は心配する必要はありません.後で、もっと良い名前をつけますので、焦らないでください.
エラー・パッケージの目的は、エラー・メッセージまたはエラーの戻り値を含むパッケージを作成することです.他の文字列ではなく文字列を選択するのが一般的です.これも容易ではないからです.文法の可読性、理解性、応用しやすいことを保証します.コピー構造やマルチパラメータ関数や戻り値は処理しません.ここでは、できるだけ簡単な例を示します.
次の例から始めましょう.
E find_slash(const char* str)
{
    int i = 0;

    while (str[i] && str[i] != '/')
          i++;

    if (str[i] == '\0')
        return fail("Error message");

    //True value
    return ret(i);
}

// . . .

auto v = find_slash(string);
if(!v)
{
    //Handle exception
}

一見、ここはC言語のスタイルに似ていますが、そうではありません.これを示すには、次の複数の関数呼び出し例を見てください.
E find_slash(const char*);
E do_some_arithmetic(int);
E<:string> format(int);
E display(std::string);

auto v = ret(string)
         .bind(find_slash)
         .bind(do_some_arithmetic)
         .bind(format)
         .bind(display);

if(!v)
{
    //Handle error
}

ここで何があったの?bindはメンバー関数で関数呼び出しをバインドし、適用してみます.エラー・ボックスに値が含まれている場合は、関数呼び出しに適用され、エラー・ボックスを返し続けます(コンパイラではエラー・ボックスを持たない関数を返すことはできません).
チェーンでfind_を呼び出しましたslashe,do_some_义齿それらはすべて誤った箱詰めを処理しないで、bind関数の作用のため、私達は関数E f(something_in)を返して結果をE f(E)関数にパラメータをします.
ここのメリットは何ですか?
もう一度,関数論理(呼び出しチェーン)とエラー処理が分離された.例外と同様に、実行がどこで中断されたのかにかかわらず、関数呼び出しチェーンを簡単に読んでコードロジックを理解することができます.実際には、関数呼び出しチェーンは、任意の呼び出し時に中断することができる.しかし、誤りは発生していないと考えることができ、もし私たちの論理が正しいならば、迅速に検査することができます.
もちろん、タイプ導出はdisplayを呼び出した後もバインドを継続することを阻止します.だから私たちもタイプ能力を失っていません.
これらの関数を他の場所で呼び出していないことに注意してください.最後にこれらの方法を組み合わせます.ここでは、小さなモジュール関数を作成する必要があります(また、テンプレート関数を作成して動作させる必要があります)値を受信し、新しい値を計算したり、失敗を返したりする必要があります.各ステップでは、エラーが発生して制御フローが中断する可能性があることを考慮し、有効な状態にあるかどうかを確認する必要はありません(異常セキュリティは、各関数呼び出しのクエリーに基づいて、関数が制御プロセスを中断しているかどうかを示し、異常が発生したら何が起こるかを示す)、これに基づいて、より安全になります.
異常と同様に,偏テンプレート関数を記述しているにもかかわらず,詳細な情報を処理することができるので,理解しやすい.
異常処理ロジックを簡単に配置し、関数呼び出しチェーンの後に置くことができます(この戻り値がさらにリンクされる必要がない限り).今、私たちは大きな実行フローを持っていて、中断していないで、小さい関数を使ってプロセスを処理して、位置決めしやすいです.新しいエラーを追加する必要がある場合は、それらの関数を見つけるだけで、関数呼び出しチェーンを通じて、処理位置に直接位置決めし、必要に応じて追加することができます.大規模なプロジェクトはより線形化され、読みやすくなります.
これで何が足りないの?
まず,これは新しい処理方式であり,C++方式とは互換性がない.これは標準的な処理方法ではありません.stlを使用する場合、例外を使用する必要があります.
私にとって、このようにするのは少し冗長です.fail(...)を明示的に書く必要があるテンプレートの導出は少し奇妙で、多態エラータイプがあればもっと悪いので、fail(「...」)を書かなければなりません.
関数に複数のパラメータがある場合の記述も困難であり,他の言語では適用タイプと抽象タイプを用いてこの問題をうまく解決できるが,これはC++では提供されない.bind 2(E ,E,f)とbind 3(E ,E,E,f),可変テンプレートパラメータ機能がより有用である.
カプセル化エラーの値を取得するには、この値が有効な値であるかどうかを確認し、「to_value」メソッドを呼び出す必要があります.私たちは検査を通さずにこれをすることはできません.オブジェクトを「解体」することを望んでいますが、これはC++ではサポートされていません.これは「次の基準に追加する」と言える特性ではありません.
これまで、読者がメンバー関数に適合する方法があるかどうか分かりませんが、考えがあれば、テストしてください.できれば、教えてください.
原子エラー処理の実装
私はそれを実現して、私はこの黒い魔法の名前を定義しました——“原子化”、あなたは“原子化”が1つの対の値と誤ったコンテキストの箱詰めだと思って、例えば、1つのboxは1つの値を含んであるいは何も1つの原子のグループではありません(ここでは1つの練習として、あなたは実現してみることができます).
奇妙なことに、キューはある角度から言えば原子グループであり、コンテキストの値を持っています.
上記のEテンプレートクラス実装から始めましょう.ここでは、C++11規格のdecltypeとauto->decltypeタイプを使用して、式のタイプを自動的に導出することができます.これは非常に役に立ちます.
ここのbind関数は少し変ですが、彼は私がさっき言った内容を実現しました.
/*
This is the "Either String" monad, as a way to handle errors.
*/

template

class E
{
private:
    //The value stored
    T m_value;
    //The error message stored
    std::string m_error;
    //A state. True it's a value, false it's the message.
    bool m_valid;

    E()
    {}

public:
    //Encapsulate the value
    static E ret(T v)
    {
        E box;
        box.m_value = v;
        box.m_valid = true;
        return box;
    }

    //Encapsulate an error
    static E fail(std::string str)
    {
        E box;
        box.m_error = str;
        box.m_valid = false;
        return box;
    }

    //True if it's a valid value
    operator bool() const
    {
        return m_valid;
    }

    //Deconstruct an E to a value
    T to_value() const
    {
        //It's a programmer error, it shouldn't happen.
        if (!*this)
        {
            std::cerr << "You can't deconstruct to a value from an error" << std::endl;
            std::terminate();
        }
        return m_value;
    }

    //Deconstruct an E to an error
    std::string to_error() const
    {
        //It's a programmer error, it shouldn't happen.
        {
            std::cerr << "You can't deconstruct to an error from a value" << std::endl;
            std::terminate();
        }
        return m_error;
    }

    friend std::ostream& operator<< (std::ostream& oss, const E& box)
    {
        if (box)
            oss << box.m_value;
        else
            oss << box.m_error;
        return oss;
    }

    template
    inline
    auto bind(F f) -> decltype(f(m_value))
    {
        using type = decltype(f(m_value));
        if (*this)
            return f(m_value);
        else
            return type::fail(m_error);
    }
};

ここで、私は重荷重<
ここでの例では、「E」タイプが必要ですが、必ずしも使用されない可能性があります.voidのために特別な重荷を実現する必要があります.ここでも同じですが、期待される値は「空き箱」です.
/*
    Special instance for void
*/

template<>
class E
{
private:
    std::string m_error;
    bool m_valid;

    E()
    {}

public:
    //Encapsulate the value
    static E ret()
    {
        E box;
        box.m_valid = true;
        return box;
    }

    //Encapsulate an error
    static E fail(std::string str)
    {
        E box;
        box.m_error = str;
        box.m_valid = false;
        return box;
    }

    //True if it's a valid value
    operator bool() const
    {
        return m_valid;
    }

    //Déconstruct an E to a value
    void to_value() const
    {
        //It's a programmer error, it shouldn't happen.
        if (!*this)
        {
            std::cerr << "You can't deconstruct to a value from an error" << std::endl;
            std::terminate();
        }
    }

    //Deconstruct an E to an error
    std::string to_error() const
    {
        //It's a programmer error, it shouldn't happen.
        if (*this)
        {
            std::cerr << "You can't deconstruct to an error from a value" << std::endl;
            std::terminate();
        }
        return m_error;
    }

    friend std::ostream& operator<< (std::ostream& oss, const E& box)
    {
        if (box)
            oss << "()";
        else
            oss << box.m_error;
        return oss;
    }

    template
    inline
    auto bind(F f) -> decltype(f())
    {
        using type = decltype(f());
        if (*this)
            return f();
        else
        return type::fail(m_error);
    }
};

retとfailの方法については言及していませんが、実際には、xxx::failとxxx::ret関数のパッケージにすぎません.
/*
   Then, I introduced those simple functions, to reduce the
   call to something readable/writable
 */
template 
inline
E ret(T v)
{
    return E::ret(v);
}

template 
inline
E fail(std::string err)
{
    return E::fail(err);
}

ここでは、上記のコードをコンパイルして実行することができます.
もっと欲しいなら、次のもっと具体的な例を試してみてください.
/*
    Here come a case of use.
*/

// What a user would see:

//Return a value in an error context
template  inline
E ret(T v);
//Fail in an error context of type T
template  inline
E fail(std::string err);

// What a user would write:

typedef std::vector<:string> vs;
typedef std::string str;

//Parse a +- formated string.
//If a letter is prefixed by +, then the function toupper is applied.
//''                                              tolower is applied.
//Non alphabetical (+ and - excepted) aren't alowed.
//Words are cut on each space ' '. Other blank characters aren't alowed.
E<:vector>> parse(std::string str)
{
    int mode = 0;
    vs vec;

    if (str.empty())
        return fail("Empty string aren't allowed");

    std::string stack;
    for(int i = 0; str[i] != '\0'; i++)
    {
        switch(str[i])
        {
        case '-':
            mode = 1;
            break;
        case '+':
            mode = 2;
            break;
        case ' ':
        {
            if(!stack.empty())
                vec.push_back(stack);
            stack.resize(0);
            mode = 0;
            break;
        }
        default:
        {
            if (!isalpha(str[i]))
                return fail("Only alpha characters are allowed");
            if (mode == 1)
                stack.push_back(tolower(str[i]));
            else if (mode == 2)
                stack.push_back(toupper(str[i]));
            else
                stack.push_back(str[i]);
            mode = 0;
            break;
        }
        }
    }
    if(!stack.empty())
        vec.push_back(stack);

    return ret(vec);
}

//Take the first word and append it to the begining of all other words.
//Vec should contain at least one element.
E<:vector>> prefixy(std::vector<:string> vec)
{
    if (vec.empty())
        return fail("Can't add prefixes on an empty table");

    std::string prefix = vec.front();
    vs out;

    for (auto s : vec)
    {
        if (prefix == s)
            continue;
        out.push_back(prefix + s + "^");
    }

    return ret(out);
}

//Concatenate all strings as a big string. Vec should contain data.
E<:string> concat(std::vector<:string> vec)
{
    std::string output;

    if (vec.empty())
        return fail("Empty vectors aren't allowed");

    for (auto s : vec)
        output += s;

    if (output.empty())
        return fail("No data found");
    return ret(output);
}

int main()
{
    typedef std::string s;

    //Parse some string, show how error interrupt computation of the "chain".
    std::cout << ret((s)"+hello   -WORLD").bind(parse).bind(prefixy).bind(concat) << std::endl;
    std::cout << ret((s)"+hello Hello  Hello").bind(parse).bind(prefixy).bind(concat) << std::endl;
    std::cout << ret((s)"+   ").bind(parse).bind(prefixy).bind(concat) << std::endl;
    std::cout << ret((s)"+hi").bind(parse).bind(prefixy).bind(concat) << std::endl;

    //Play with lambda to "replace" a value if it's not an error.
    std::cout << ret((s)"Some string").bind([](const std::string&) {return fail("Failed");});
    std::cout << ret(23).bind([](const int) {return ret(42);});
    std::cout << fail("NaN").bind([](const int) {return ret(42);});

    return 0;
}

译文:An alternative error handling strategy for C++転載自:伯楽オンライン-周昌鴻