分享方式:


如何:例外和非例外程式碼之間的介面

本文說明如何在 C++ 程式碼中實作一致的例外狀況處理,以及如何在例外狀況界限的錯誤碼中轉譯例外狀況。

有時候 C++ 程式碼必須與不使用例外狀況的程式碼介面(非例外狀況程式碼)。 這類介面稱為 例外狀況界限 。 例如,可以在 C++ 程式中呼叫 Win32 函式 CreateFileCreateFile 不會擲回例外狀況。 相反地,它會設定函式可以擷 GetLastError 取的錯誤碼。 如果您的 C++ 程式不簡單,您可能偏好有一致的例外狀況型錯誤處理原則。 而且,您可能不想因為與非例外程式碼介面而放棄例外狀況。 您也不想在 C++ 程式碼中混合以例外狀況為基礎的和非例外狀況型錯誤原則。

從 C++ 呼叫非例外函式

當您從 C++ 呼叫非例外狀況函式,這個概念是將函式包裝在可偵測所有錯誤然後擲回例外狀況的 C++ 函式中。 當您設計這類包裝函式時,請先決定要提供的例外狀況保證類型:noexcept、strong 或 basic。 其次,設計函式,以致擲回例外狀況時正確地釋放所有資源,例如,檔案控制代碼。 一般而言,這表示您使用智慧型指標或類似的資源管理員來擁有資源。 如需設計考慮的詳細資訊,請參閱 如何:設計例外狀況安全性

範例

下列範例顯示 C++ 函式內部使用 Win32 CreateFileReadFile 函式開啟和讀取兩個檔案。 File 類別是檔案控制代碼的「資源擷取為初始設定」(Resource Acquisition Is Initialization,RAII) 包裝函式。 其建構函式會偵測到「找不到檔案」條件,並擲回例外狀況,以將錯誤傳播至 C++ 可執行檔的呼叫堆疊(在此範例中為 main() 函式)。 如果於 File 物件完全建構之後擲回例外狀況,解構函式會被自動呼叫 CloseHandle 以釋放檔案控制代碼 (如果您想要的話,您可以使用 Active Template Library (ATL) CHandle 類別進行此相同用途,或 unique_ptr 搭配自訂刪除函式。 呼叫 Win32 和 CRT API 的函式會偵測錯誤,然後使用本機定義的 ThrowLastErrorIf 函式擲回 C++ 例外狀況,進而使用 Win32Exception 衍生自 類別的 runtime_error 類別。 此範例中的所有函式都提供強式例外狀況保證:如果這些函式中的任何時間點擲回例外狀況,則不會外泄任何資源,也不會修改任何程式狀態。

// compile with: /EHsc
#include <Windows.h>
#include <stdlib.h>
#include <vector>
#include <iostream>
#include <string>
#include <limits>
#include <stdexcept>

using namespace std;

string FormatErrorMessage(DWORD error, const string& msg)
{
    static const int BUFFERLENGTH = 1024;
    vector<char> buf(BUFFERLENGTH);
    FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, 0, error, 0, buf.data(),
        BUFFERLENGTH - 1, 0);
    return string(buf.data()) + "   ("  + msg  + ")";
}

class Win32Exception : public runtime_error
{
private:
    DWORD m_error;
public:
    Win32Exception(DWORD error, const string& msg)
        : runtime_error(FormatErrorMessage(error, msg)), m_error(error) { }

    DWORD GetErrorCode() const { return m_error; }
};

void ThrowLastErrorIf(bool expression, const string& msg)
{
    if (expression) {
        throw Win32Exception(GetLastError(), msg);
    }
}

class File
{
private:
    HANDLE m_handle;

    // Declared but not defined, to avoid double closing.
    File& operator=(const File&);
    File(const File&);
public:
    explicit File(const string& filename)
    {
        m_handle = CreateFileA(filename.c_str(), GENERIC_READ, FILE_SHARE_READ,
            nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
        ThrowLastErrorIf(m_handle == INVALID_HANDLE_VALUE,
            "CreateFile call failed on file named " + filename);
    }

    ~File() { CloseHandle(m_handle); }

    HANDLE GetHandle() { return m_handle; }
};

size_t GetFileSizeSafe(const string& filename)
{
    File fobj(filename);
    LARGE_INTEGER filesize;

    BOOL result = GetFileSizeEx(fobj.GetHandle(), &filesize);
    ThrowLastErrorIf(result == FALSE, "GetFileSizeEx failed: " + filename);

    if (filesize.QuadPart < (numeric_limits<size_t>::max)()) {
        return filesize.QuadPart;
    } else {
        throw;
    }
}

vector<char> ReadFileVector(const string& filename)
{
    File fobj(filename);
    size_t filesize = GetFileSizeSafe(filename);
    DWORD bytesRead = 0;

    vector<char> readbuffer(filesize);

    BOOL result = ReadFile(fobj.GetHandle(), readbuffer.data(), readbuffer.size(),
        &bytesRead, nullptr);
    ThrowLastErrorIf(result == FALSE, "ReadFile failed: " + filename);

    cout << filename << " file size: " << filesize << ", bytesRead: "
        << bytesRead << endl;

    return readbuffer;
}

bool IsFileDiff(const string& filename1, const string& filename2)
{
    return ReadFileVector(filename1) != ReadFileVector(filename2);
}

#include <iomanip>

int main ( int argc, char* argv[] )
{
    string filename1("file1.txt");
    string filename2("file2.txt");

    try
    {
        if(argc > 2) {
            filename1 = argv[1];
            filename2 = argv[2];
        }

        cout << "Using file names " << filename1 << " and " << filename2 << endl;

        if (IsFileDiff(filename1, filename2)) {
            cout << "+++ Files are different." << endl;
        } else {
            cout<< "=== Files match." << endl;
        }
    }
    catch(const Win32Exception& e)
    {
        ios state(nullptr);
        state.copyfmt(cout);
        cout << e.what() << endl;
        cout << "Error code: 0x" << hex << uppercase << setw(8) << setfill('0')
            << e.GetErrorCode() << endl;
        cout.copyfmt(state); // restore previous formatting
    }
}

從非例外狀況程式碼呼叫例外狀況程式碼

C 程式可以呼叫宣告為 extern "C" 的 C++ 函式。 C++ COM 伺服器可由任意數目不同語言撰寫的程式碼取用。 當您實作由非例外狀況程式碼呼叫的 C++ 公用例外狀況感知函式,C++ 函式不應該允許任何例外狀況傳播回呼叫端。 這類呼叫端無法攔截或處理 C++ 例外狀況。 程式可能會終止、流失資源或造成未定義的行為。

我們建議您 extern "C" 的 C++ 函式特別攔截它知道如何處理的每個例外狀況,如果適當的話,請將例外狀況轉換成呼叫端瞭解的錯誤碼。 如果不是所有可能的例外狀況都是已知,C++ 函式應該具有 catch(...) 區塊做為最後一個處理常式。 在這種情況下,最好向呼叫端回報嚴重錯誤,因為您的程式可能處於未知且無法復原的狀態。

下列範例顯示一個函式,假設可能擲回的任何例外狀況都是 Win32Exception 或衍生自 std::exception 的例外狀況類型。 函式攔截這些類型的任何例外狀況,並將錯誤資訊做為 Win32 錯誤碼傳遞給呼叫端。

BOOL DiffFiles2(const string& file1, const string& file2)
{
    try
    {
        File f1(file1);
        File f2(file2);
        if (IsTextFileDiff(f1, f2))
        {
            SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
            return FALSE;
        }
        return TRUE;
    }
    catch(Win32Exception& e)
    {
        SetLastError(e.GetErrorCode());
    }

    catch(std::exception& e)
    {
        SetLastError(MY_APPLICATION_GENERAL_ERROR);
    }
    return FALSE;
}

當您從例外狀況轉換成錯誤碼時,可能會發生問題:錯誤碼通常不包含例外狀況可儲存的資訊豐富性。 若要解決此問題,您可以為每個可能擲回的特定例外狀況類型提供區塊 catch ,並執行記錄來記錄例外狀況的詳細資料,再轉換成錯誤碼。 如果多個函式都使用相同的區塊集 catch ,此方法可能會建立重複的程式碼。 避免程式碼重複的好方法是將這些區塊重構為一個實作 和 區塊並 catch 接受 區塊中 try 叫用的函式物件的私人公用程式函 try 式。 在每個公用函式,將程式碼做為 Lambda 運算式傳遞至這個公用程式函式。

template<typename Func>
bool Win32ExceptionBoundary(Func&& f)
{
    try
    {
        return f();
    }
    catch(Win32Exception& e)
    {
        SetLastError(e.GetErrorCode());
    }
    catch(const std::exception& e)
    {
        SetLastError(MY_APPLICATION_GENERAL_ERROR);
    }
    return false;
}

下列範例顯示如何撰寫定義仿函式的 Lambda 運算式。 Lambda 運算式通常比呼叫具名函式物件的程式碼更容易內嵌讀取。

bool DiffFiles3(const string& file1, const string& file2)
{
    return Win32ExceptionBoundary([&]() -> bool
    {
        File f1(file1);
        File f2(file2);
        if (IsTextFileDiff(f1, f2))
        {
            SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
            return false;
        }
        return true;
    });
}

如需 Lambda 運算式的詳細資訊,請參閱 Lambda 運算式

透過例外狀況程式碼從例外程式碼呼叫例外狀況程式碼

在未察覺到例外狀況的程式碼之間擲回例外狀況,但不建議這麼做。 例如,您的 C++ 程式可能會呼叫使用您提供的回呼函式的程式庫。 在某些情況下,您可以在原始呼叫端可以處理的非例外程式碼中,從回呼函式擲回例外狀況。 不過,例外狀況可以順利運作的情況是嚴格的。 您必須以保留堆疊回溯語意的方式編譯程式庫程式碼。 例外狀況不知道程式碼無法執行任何可能設陷 C++ 例外狀況的任何動作。 此外,呼叫端與回呼之間的程式庫程式碼無法配置本機資源。 例如,非例外狀況感知的程式碼不能有指向已配置堆積記憶體的區域變數。 當堆疊解除復原時,這些資源會外泄。

必須符合這些需求,才能跨非例外狀況感知程式碼擲回例外狀況:

  • 您可以使用 ,跨非例外狀況感知程式碼建置整個程式碼 /EHs 路徑。
  • 當堆疊解除復原時,沒有任何本機配置的資源可能會流失。
  • 程式碼沒有任何 __except 攔截所有例外狀況的結構化例外狀況處理常式。

因為跨非例外狀況程式碼擲回例外狀況很容易發生錯誤,而且可能會導致難以偵錯的挑戰,因此不建議這麼做。

另請參閱

例外狀況和錯誤處理的新式 C++ 最佳做法
如何:設計例外狀況安全性