방법: 예외 코드와 예외가 아닌 코드 간의 인터페이스
이 문서에서는 C++ 코드에서 일관된 예외 처리를 구현하는 방법과 예외 경계에서 오류 코드와 예외를 변환하는 방법을 설명합니다.
경우에 따라 C++ 코드는 예외(예외가 아닌 코드)를 사용하지 않는 코드와 인터페이스해야 합니다. 이러한 인터페이스를 예외 경계라고 합니다. 예를 들어 C++ 프로그램에서 CreateFile
Wind32 함수를 호출할 수 있습니다. CreateFile
는 예외를 throw하지 않습니다. 대신 함수에서 검색 GetLastError
할 수 있는 오류 코드를 설정합니다. C++ 프로그램이 사소하지 않은 경우 일관된 예외 기반 오류 처리 정책을 사용하는 것이 좋습니다. 또한 예외가 아닌 코드와 인터페이스를 사용하므로 예외를 중단하지 않을 수 있습니다. 또한 C++ 코드에서 예외 기반 및 예외 기반이 아닌 오류 정책을 혼합하지 않습니다.
C++에서 예외가 아닌 함수 호출
C++에서 비예외 함수를 호출하는 경우 오류를 검색한 다음 예외를 throw할 수 있는 C++ 함수에서 해당 함수를 래핑합니다. 이러한 래퍼 함수를 디자인할 때 먼저 제공할 예외 보장 유형(noexcept, strong 또는 basic)을 결정합니다. 두 번째로 예외가 throw된 경우 모든 리소스(예: 파일 핸들)가 올바르게 해제되도록 함수를 디자인합니다. 일반적으로 스마트 포인터 또는 유사한 리소스 관리자를 사용하여 리소스를 소유한다는 의미입니다. 디자인 고려 사항에 대한 자세한 내용은 방법: 예외 안전을 위한 설계를 참조하세요.
예시
다음 예제에서는 Win32 CreateFile
및 ReadFile
함수를 사용하여 내부적으로 두 파일을 읽고 여는 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하는 것은 오류가 발생하기 쉽고 디버깅 문제가 발생할 수 있으므로 권장하지 않습니다.
참고 항목
피드백
https://aka.ms/ContentUserFeedback
출시 예정: 2024년 내내 콘텐츠에 대한 피드백 메커니즘으로 GitHub 문제를 단계적으로 폐지하고 이를 새로운 피드백 시스템으로 바꿀 예정입니다. 자세한 내용은 다음을 참조하세요.다음에 대한 사용자 의견 제출 및 보기