天天看點

C++的另一種錯誤處理政策

這篇短文是讨論一個大多數程式員都感興趣的一個話題:錯誤處理。錯誤處理是程式設計的一個“黑暗面”。它既是應用程式的“現實世界”的關鍵點,也是一個你想隐藏的複雜業務。

在早期的C程式設計生涯中,我知道三種錯誤處理的方式。

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的增強,引入了一種新的錯誤處理方式——異常。異常通過抛出一個錯誤的方式來中斷正常代碼執行邏輯,并可以被其他地方所捕獲。下面是一個簡單的例子:

        throw AnException("Error message");

try

    find_slash(string);

catch(AnException& e)

   //Handle exception

這樣做的好處?

程式邏輯和錯誤處理分離了。一邊你可以看到函數是如何工作的,而另一邊你可以看到函數失敗時候是怎麼處理的。這樣做很完美,可以很容易看出錯誤處理和正常程式邏輯。

另外,現在你可以為你的錯誤提供你需要的盡可能多的資訊,因為你可以将需要的内容填充在自定義異常對象裡。

這樣做的壞處

編寫詳盡的異常處理變得很冗。你需要一個異常樹,但是最好不要太大,這樣,你可以選擇捕獲感興趣的異常。同時,内部需要提供錯誤碼,來獲知究竟發生了什麼,同時需要檢索一些錯誤消息,等等。編寫寫異常類通常都是冗長,這是将資訊嵌入到錯誤裡來靈活處理更多的資訊的成本。

這裡的錯誤處理哲學是将錯誤盡可能推遲到需要處理的地方再處理,當你不知道程式執行過程究竟哪裡會産生一個錯誤,你需要跳過不同的檔案和功能函數來查找,這通常都是困難的,如果你在一個很深的調用樹(這裡意思是當你将函數調用繪制出一個圖形,其形狀類似一棵樹)上引發了一個異常,你需要指定在哪裡來處理這個異常,當它被處理的時候,它又是在哪裡發生的。特别是當你的程式很大,又是很早之前編寫,有恰巧設計不夠良好的時候,就更加顯得困難。而大多數商業項目都是這樣。

是以我覺得“異常是危險的”。雖然它提供了一種良好的方式來處理錯誤——僅限于一些小項目,并且這裡的調用圖簡單且易于掌握時候。

錯誤封裝的模式

我這裡把它叫做一種模式,是以人們不必害怕擔心。後面,我會給它一種更好的命名,是以請不要着急。

錯誤封裝的主旨是建立一種封裝來包含錯誤消息或者錯誤的傳回值。我們通常會選擇字元串而不是其他,因為這也并不容易實作。我們盡力保證文法的可讀性,可了解,并且容易應用。我們不處理拷貝構造或者多參數函數及傳回值,這裡僅給出一個盡可能簡單的例子。

讓我們以下面的例子開始:

E<int> find_slash(const char* str)

        return fail<int>("Error message");

    return ret(i);

auto v = find_slash(string);

if(!v)

    //Handle exception

乍一看,這裡有點類似C語言的風格,但是不是,為表明這一點,請看接下來的多個函數調用例子:

E<int> find_slash(const char*);

E<int> do_some_arithmetic(int);

E<std::string> format(int);

E<void> display(std::string);

auto v = ret(string)

         .bind(find_slash)

         .bind(do_some_arithmetic)

         .bind(format)

         .bind(display);

    //Handle error

好了,這裡發生了什麼?bind是一個成員函數來綁定你的函數調用,試着去應用它。如果錯誤裝箱裡面含有一個值,那麼它就應用于函數調用,繼續傳回一個錯誤裝箱(編譯器不允許你傳回一個不帶錯誤裝箱的函數)。

是以,我們鍊式調用了find_slashe,do_some_arithmetic, format和display.它們都不處理錯誤裝箱,由于bind函數的作用,我們将函數E f(something_in)傳回結果給E f(E)函數做參數。

這裡的好處是什麼?

再一次,函數邏輯(調用鍊)和錯誤處理分離了。和異常一樣,我們可以簡單讀一下函數調用鍊來了解代碼邏輯,而不用關心執行是在哪裡被中斷的。事實上,函數調用鍊可以在任何調用時被中斷。但是我們可以認為沒有錯誤發生,如果我們的邏輯是正确的,可以很快速檢查。

當然,類型推導會阻止你在調用display之後繼續進行綁定。是以我們也沒有失去類型能力。

注意,我們沒有在其他地方調用這些函數,我們在最後将這些方法組裝在一起。這裡是關鍵,你應該編寫一些小的子產品函數(另外,注意:你應該編寫模闆函數使其工作)接收一個值,然後計算一個新值或者傳回失敗。在每一步中,你都不需要考慮可能出現錯誤導緻你的控制流中斷,并且校驗你是否在一個有效的狀态上(異常安全基于查詢每個函數調用,指出函數是否中斷你的控制流程,如果出現異常會發生什麼),基于這一點,這樣做更安全。

和異常一樣,我們可以處理很詳細的資訊,盡管這裡我們編寫的是一個偏模闆函數,是以也容易了解一些。

我們可以很容易放置異常處理邏輯,把它放在函數調用鍊之後(除非這個傳回值還需要進一步被連結)。現在,我們有一個大的的執行流,沒有中斷,使用小的函數處理流程,容易定位。當需要添加一個新的錯誤時,你隻需找到那些函數,通過函數調用鍊,你可以直接定位到處理位置,并根據需要添加。大型項目變得更加的線性化,并且更易讀。

這樣做有什麼不足?

首先,這是一個新的處理方式,并且和C++的方式不相容。這不是一個标準處理方法,當你使用stl時,你仍然需要使用異常。

對于我來說,這樣做還是有點冗長。需要顯式編寫fail(…)的模闆推導顯得有點怪異,如果你有個多态錯誤類型就更糟了,你不得不這樣寫fail<return_type, error_type>("...").

當函數有多個參數時編寫也很困難,在其他一些語言中,可以使用适用類型和抽象類型很好地解決這個問題,不過這在C++中不會提供。我想更适合使用bind2(E<a>, E<b>, f)和bind3(E<a>, E<b>, E<c>, f),可變模闆參數功能更有用。

為擷取封裝錯誤中的值,我們需要檢查這個值是否是有效值,接着調用一個“to_value”方法。我們沒辦法不通過檢查來做到這一點。我們希望的是“解構”一個對象,不過這在C++中不支援,這也不是一些可以說“我們把它加入到下一個标準”的特性。

目前為止,我不知道讀者是否有方法将其适配到成員函數中,如果你有想法,請測試一下,如果可以,請告知我們。

實作原子錯誤處理

我實作了它,我定義了這個黑魔法的名字——“原子化”,你可以認為“原子化”是一個對值和錯誤上下文的裝箱,比如,一個box包含一個值或者什麼也不包含是一個原子組(這裡作為一個練習,你可以試着實作一下)。

有點奇怪的是,從某個角度來說隊列是一個原子組,他們擁有一個上下文的值。

讓我們從上面的E模版類實作開始,這裡使用了C++11标準中的decltype和 auto -> decltype 類型,允許自動推導得到表達式的類型,這非常有用。

這裡的bind函數有點怪異,但是他實作了我剛才提到的内容。

/*

This is the "Either String" monad, as a way to handle errors.

*/

template

<typename T>

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)

        box.m_error = str;

        box.m_valid = false;

    //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

            std::cerr << "You can't deconstruct to an error from a value" << std::endl;

        return m_error;

    friend std::ostream& operator<< (std::ostream& oss, const E<T>& box)

        if (box)

            oss << box.m_value;

        else

            oss << box.m_error;

        return oss;

    template<typename F>

    inline

    auto bind(F f) -> decltype(f(m_value))

        using type = decltype(f(m_value));

        if (*this)

            return f(m_value);

            return type::fail(m_error);

};

這裡,我重載了<<運算符,是以導出裝箱中的内容更容易一些。我們并不是一定需要它,在“真”值時去掉這一點也更好一些。

這裡的例子,我們需要一個“E”類型,但是它可能不一定使用。我們需要為void實作一個特别的重載,這裡其實也是一樣的,隻不過期望的值是一個“空箱”。

    Special instance for void

template<>

class E<void>

    static E ret()

    //Déconstruct an E to a value

    void to_value() const

    friend std::ostream& operator<< (std::ostream& oss, const E<void>& box)

            oss << "()";

    auto bind(F f) -> decltype(f())

        using type = decltype(f());

            return f();

        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 <typename T>

inline

E<T> ret(T v)

    return E<T>::ret(v);

E<T> fail(std::string err)

    return E<T>::fail(err);

這裡,你可以編譯并執行一下上面的代碼。

如果你想要更多的,可以試試下面這個更具體一點的例子:

    Here come a case of use.

// What a user would see:

//Return a value in an error context

template <typename T> inline

E<T> ret(T v);

//Fail in an error context of type T

E<T> fail(std::string err);

// What a user would write:

typedef std::vector<std::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<std::vector<std::string>> parse(std::string str)

    int mode = 0;

    vs vec;

    if (str.empty())

        return fail<vs>("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;

        case ' ':

            if(!stack.empty())

                vec.push_back(stack);

            stack.resize(0);

            mode = 0;

        default:

            if (!isalpha(str[i]))

                return fail<vs>("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]);

    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<std::vector<std::string>> prefixy(std::vector<std::string> vec)

    if (vec.empty())

        return fail<vs>("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<std::string> concat(std::vector<std::string> vec)

    std::string output;

        return fail<str>("Empty vectors aren't allowed");

        output += s;

    if (output.empty())

        return fail<str>("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<s>("Failed");});

    std::cout << ret(23).bind([](const int) {return ret(42);});

    std::cout << fail<int>("NaN").bind([](const int) {return ret(42);});

    return 0;

繼續閱讀