방법: 예외 코드와 예외가 아닌 코드 간의 인터페이스

이 문서에서는 C++ 코드에서 일관된 예외 처리를 구현하는 방법과 예외 경계에서 오류 코드와 예외를 변환하는 방법을 설명합니다.

경우에 따라 C++ 코드는 예외(예외가 아닌 코드)를 사용하지 않는 코드와 인터페이스해야 합니다. 이러한 인터페이스를 예외 경계라고 합니다. 예를 들어 C++ 프로그램에서 CreateFile Wind32 함수를 호출할 수 있습니다. CreateFile 는 예외를 throw하지 않습니다. 대신 함수에서 검색 GetLastError 할 수 있는 오류 코드를 설정합니다. C++ 프로그램이 사소하지 않은 경우 일관된 예외 기반 오류 처리 정책을 사용하는 것이 좋습니다. 또한 예외가 아닌 코드와 인터페이스를 사용하므로 예외를 중단하지 않을 수 있습니다. 또한 C++ 코드에서 예외 기반 및 예외 기반이 아닌 오류 정책을 혼합하지 않습니다.

C++에서 예외가 아닌 함수 호출

C++에서 비예외 함수를 호출하는 경우 오류를 검색한 다음 예외를 throw할 수 있는 C++ 함수에서 해당 함수를 래핑합니다. 이러한 래퍼 함수를 디자인할 때 먼저 제공할 예외 보장 유형(noexcept, strong 또는 basic)을 결정합니다. 두 번째로 예외가 throw된 경우 모든 리소스(예: 파일 핸들)가 올바르게 해제되도록 함수를 디자인합니다. 일반적으로 스마트 포인터 또는 유사한 리소스 관리자를 사용하여 리소스를 소유한다는 의미입니다. 디자인 고려 사항에 대한 자세한 내용은 방법: 예외 안전을 위한 설계를 참조하세요.

예시

다음 예제에서는 Win32 CreateFileReadFile 함수를 사용하여 내부적으로 두 파일을 읽고 여는 C++ 함수를 보여 줍니다. File 클래스는 파일 핸들에 대한 RAII(Resource Acquisition Is Initialization) 래퍼입니다. 생성자는 "파일을 찾을 수 없음" 조건을 검색하고 C++ 실행 파일의 호출 스택 위로 오류를 전파하는 예외를 throw합니다(이 예제 main() 에서는 함수). File 개체가 완전히 생성된 후 예외가 throw된 경우 소멸자는 파일 핸들을 해제하기 위해 자동으로 CloseHandle을 호출합니다. 원하는 경우 동일한 용도로 ATL(Active Template Library) CHandle 클래스를 사용하거나 unique_ptr 사용자 지정 삭제 함수와 함께 사용할 수 있습니다. Win32 및 CRT API를 호출하는 함수는 오류를 검색한 다음 로컬로 정의된 ThrowLastErrorIf 함수를 사용하여 C++ 예외를 throw합니다. 그러면 클래스에서 파생된 클래스가 runtime_error 사용됩니다Win32Exception. 이 예제의 모든 함수는 강력한 예외 보장을 제공합니다. 이러한 함수의 어느 시점에서든 예외가 throw되면 리소스가 유출되지 않고 프로그램 상태가 수정되지 않습니다.

// 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++ 예외를 catch하거나 처리할 방법이 없습니다. 프로그램이 종료되거나, 리소스가 누출되거나, 정의되지 않은 동작이 발생할 수 있습니다.

C++ 함수는 특히 처리 방법을 알고 있는 모든 예외를 catch하고, 적절한 경우 예외를 호출자가 이해하는 오류 코드로 변환하는 것이 좋습니다 extern "C" . 일부 잠재적 예외를 알지 못하는 경우 C++ 함수에는 마지막 처리기로 catch(...) 블록이 있어야 합니다. 이러한 경우 프로그램이 알 수 없고 복구할 수 없는 상태일 수 있으므로 호출자에게 치명적인 오류를 보고하는 것이 가장 좋습니다.

다음 예제에서는 throw될 수 있는 예외가 파생된 std::exception예외 형식 또는 예외 형식이라고 Win32Exception 가정하는 함수를 보여 줍니다. 함수는 이러한 형식의 예외를 catch하고 호출자에게 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;
}

예외에서 오류 코드로 변환하는 경우 잠재적인 문제가 있습니다. 오류 코드에는 예외가 저장할 수 있는 다양한 정보가 포함되지 않는 경우가 많습니다. 이 문제를 해결하려면 throw될 수 있는 각 특정 예외 유형에 대한 블록을 제공하고 catch 로깅을 수행하여 오류 코드로 변환되기 전에 예외의 세부 정보를 기록할 수 있습니다. 이 방법은 여러 함수가 모두 동일한 블록 집합 catch 을 사용하는 경우 반복 코드를 만들 수 있습니다. 코드 반복을 방지하는 좋은 방법은 해당 블록을 구현 try 하고 catch 블록에서 try 호출되는 함수 개체를 허용하는 하나의 프라이빗 유틸리티 함수로 리팩터링하는 것입니다. 각 공용 함수에서 람다 식으로 유틸리티 함수에 코드를 전달합니다.

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;
}

다음 예제에서는 함수를 정의하는 람다 식을 작성하는 방법을 보여 줍니다. 람다 식은 명명된 함수 개체를 호출하는 코드보다 인라인으로 읽기가 더 쉬운 경우가 많습니다.

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;
    });
}

람다 식에 대한 자세한 내용은 람다 식을 참조하세요.

예외 코드에서 예외가 아닌 코드를 통해 예외적 코드 호출

예외를 인식하지 않는 코드에서 예외를 throw하는 것은 가능하지만 권장되지는 않습니다. 예를 들어 C++ 프로그램에서 제공하는 콜백 함수를 사용하는 라이브러리를 호출할 수 있습니다. 경우에 따라 원래 호출자가 처리할 수 있는 예외가 아닌 코드를 통해 콜백 함수에서 예외를 throw할 수 있습니다. 그러나 예외가 성공적으로 작동할 수 있는 상황은 엄격합니다. 스택 해제 의미 체계를 유지하는 방식으로 라이브러리 코드를 컴파일해야 합니다. 예외를 인식하지 않는 코드는 C++ 예외를 트래핑할 수 있는 작업을 수행할 수 없습니다. 또한 호출자와 콜백 간의 라이브러리 코드는 로컬 리소스를 할당할 수 없습니다. 예를 들어 예외를 인식하지 않는 코드에는 할당된 힙 메모리를 가리키는 로컬이 있을 수 없습니다. 이러한 리소스는 스택이 해제될 때 유출됩니다.

예외를 인식하지 않는 코드에서 예외를 throw하려면 다음 요구 사항을 충족해야 합니다.

  • 를 사용하여 /EHs예외 인식이 아닌 코드에서 전체 코드 경로를 빌드할 수 있습니다.
  • 스택이 해제될 때 누수될 수 있는 로컬로 할당된 리소스가 없습니다.
  • 코드에는 __except 모든 예외를 catch하는 구조적 예외 처리기가 없습니다.

예외가 아닌 코드에서 예외를 throw하는 것은 오류가 발생하기 쉽고 디버깅 문제가 발생할 수 있으므로 권장하지 않습니다.

참고 항목

예외 및 오류 처리에 대한 최신 C++ 모범 사례
방법: 예외 보안을 위한 디자인