Modern C++

[C++] C++의 Exception Handling과 Exception Mechanism

dev-ohdam 2024. 12. 13. 16:54

 

 
개요

 우리가 프로그램을 개발하는 것에 있어서 중요한 요소는 기능, 최적화 등 다양한 요소가 있지만 가장 중요한 것은 프로그램 동작

과정에서 발생한 문제를 빠르게 발견하고 해결하는 것이다. 그러기 위해선, 당연하게도 문제가 발생한 위치를 파악해야 하는데,

이 과정은 쉽지 않다.

 

 만약, 컴파일러에 의해 에러를 사전에 발견할 수 있다면, 아주 운이 좋은 상황이거나, 그다지 큰 문제가 아니다.

하지만 대부분의 문제점은 반드시 런타임(RunTime) 중에 발생한다. 그렇다고 매번 똑같은 시점에 에러가 발생된다?

그것 또한 아니다. 언제 어떠한 원인으로 발생할지 아무도 모른다. 이는 우리가 백날 코드를 살펴본다 한들 파악하기 어렵다. 

 

 어떻게 하면 이러한 문제점을 쉽게 파악할 수 있을까?라고 하면 다음과 같은 방법을 사용할 수 있다.

Output Stream 함수들을 활용해 프로그램의 모든 동작을 출력해 보면 된다. 무식하지만 가장 쉬운 방법이다.

하지만, 이러한 입출력 함수들의 경우, 퍼포먼스를 중요하게 여기는 전문적인 프로그램에서 채택하긴 어렵다.

 

 이를 해결하기 위해, C++, JAVA, C# 등 다양한 언어들은 런타임 중에 발생하는 예외들을 처리하기 위한 문법을 제공한다.

오늘은 이에 관해 다뤄볼 것이다. 그리고 오늘 다루는 내용 중 noexcept 키워드를 이해하면 추후 다룰 Move Semantic

이해하는 과정에서도 많은 도움이 된다.

 

 

 Error Commnunication

 기본적으로 에러가 발생한다면, 사용자(or 개발자)에게 에러에 관한 정보를 전달하여 소통을 수행해야 한다.

이 과정을 Error Communication이라고 한다.

 

 예를 들어, 프로그램 내에서 비정상적인 행동을 했거나, 특정 문제가 발생했을 때 뜨는 팝업창은 대표적인 Error Communication 방법이다. 현재 어떤 문제가 발생했는지, 사용자가 이후 어떻게 동작을 수행하면 되는지 등의 가이드라인을 사용자에게 제시한다.

 

 High Level Error Handling

 이렇듯, 에러에 관한 정보를 보여주기 위해, 보편적으로 Dialog와 같은 GUI 시스템을 많이 채택한다.

하지만 무분별한 GUI 시스템 활용은 오히려 프로그램을 복잡하고 산만하게 만들 수 있다.

GUI를 출력하기 위한 코드와 예외 처리를 위한 코드가 한 곳에 뭉쳐 얽혀있다고 상상하면, 정말 머리가 아프다.

 

 그렇기 때문에, Error Communication을 좀 더 효율적으로 처리하기 위해선

예외 처리 부분(Error Handler)과 출력 부분(Error Display)을 분리해서 따로 처리하는 것이 좋다.

 

예외 처리 방법 1 - Error Code 방식 (C98 ~)

 자 그렇다면 GUI와 예외 처리 부분을 분리해야 한다는 사실은 알게 됐다.

그렇다면 에러 발생 지점에서 발생한 Error Condition(에러 정보)을 예외 처리 프로세서에 전달하는 방법은 무엇인가? 에 관해
과거에는 다음과 같은 방법을 사용했다.

 

 바로 Error Code(에러 코드)를  사용하면 된다.

구현 방식은 아래와 같다.

 

  1. 함수의 리턴 값을 에러와 일치하는 코드 넘버로 설정한다.
  2. 호출자(Caller)는 해당 함수의 리턴 값을 체크한다.
  3. 호출자가 해당 에러를 직접 처리한다.(에러 코드 사용 방법 1)
  4. 혹은 다른 상위 개념의 대리자(Handler)에게 해당 리턴 값을 넘겨준다. (에러 코드 사용 방법 2)

 보통 소켓 프로그래밍을 수행할 때, 이 방식을 많이 활용해 봤을 것이다. Network I/O API를 호출하면 (send, recv...)

API가 반환하는 에러 코드를 통해 예외 처리를 수행할 수 있었다. (보통 switch 문으로 이를 관리했다.)

 

하지만 해당 방식에는 단점이 존재하는데, 다음과 같다.

  1. 코드를 복잡하게 만든다. 잠재적으로 에러가 발생 가능한 코드 부분에 매번 분기를 나눠 이를 검증하고 처리해야 한다.
    if(err_code == 에러코드1) {/* ... */}
    else if(err_code == 에러코드2) { /*... */}
    else {} // ....
  2. 유지보수가 어렵다. 에러 코드의 종류가 많아질수록 이를 분할해서 관리해야 하고, 이 과정에서 분할된 에러 코드 간의
    동기화 작업 또한 번거롭다.

  3. C++ 언어 개발 환경에서 클래스의 생성자에서는 에러 코드를 반환할 수 없다. (생성자는 아무것도 반환하지 않는다.)

1, 2번 이유도 크지만, 클래스 사용이 빈번한 C++언어 특성상, 객체의 생성과정에서 발생하는 예외를 처리할 수 없다는 점은

치명적인 문제이다.

예외 처리 방법 2 - 표준 예외 객체(C++11~)

 C++ 에선 Error Code 대신 Exception 객체를 제공한다. 런타임 도중, 에러가 발생했을 경우, Exception 객체를 생성하고, 이 Exception 타입에 따라 적절한 Handler를 탐색한다. 이때 Handler는 Exception 객체의 내부 정보를 기반으로 하여 문제 해결을 위한 추가적인 정보를 얻을 수도 있다.

 

 당연히 Exception 객체의 타입이나 정보는 개발자가 코드로 작성하여 직접 제공해야 한다.

 

 예외가 발생하지 않더라도 내가 원하는 시점이나 원하는 조건 상황에서 직접 Exception 객체를 생성 (Throwing Exception) 할 수 있다. 그리고 컴파일러는 생성된 Exception 객체를 적절한 Handler에게 전달하게 된다. 이렇게 컴파일러가 흐름을 제어하는 것은 매우 안전한 동작 방식이라고 볼 수 있다. 그리고 Exception Type이 추가되더라도 이를 위한 추가적인 분기 작성이 불필요하기 때문에 유지보수면에서도 유용하다.

 

 만약, Exception 객체를 처리하기 위한 적절한 Handler가 없을 경우 프로그램은 그 즉시 종료된다.(Terminate)

Exception 객체는 클래스의 형태로 제공되며, 단일 숫자, 문자만 지정할 수 있는 에러 코드와 달리, 복합적인 정보를 담을 수 있다.

 

 C++11부턴 이 Exception 객체에 관한 표준 클래스를 제공하는데, 바로 std::exception이다.

그 이유는 일관성 있는 예외 처리를 위함이다. 과거에는 예외를 throwing 할 때, 생성하는 Error Condition에 관해 특정한 규격이 정해져 있지 않았다. 정수값을 throw 할 수도 있고, 문자값을 throw할 수도 있다. 이렇게 통일되지 않은 Exception 객체들은 오히려 개발 과정에서 혼란을 야기시킬 수 있다.

 

 해당 문제에 관해 std::exception의 경우, 다형성(Polymorphism)을 지원하기 때문에 여러 예외 타입을 일관된 인터페이스로

구현하고 관리할 수 있다.

 

std::exception과 std::exception Hiearachy

(좌) https://www.javatpoint.com/cpp-exception-handling (우) cppReference

 자 그렇다면 C++11부터 제공하는 Exception 객체인 std::exception에 관해서 알아보자.

std::exception모든 표준 예외 클래스의 Base 클래스라고 볼 수 있다. 그리고 이를 상속받는 다양한 하위 예외 클래스들이

존재한다. std::exception은 위에서도 언급했듯 일관된 인터페이스를  제공한다.

 

 해당 게시글에서 모든 하위 예외 클래스에 관해서 자세하게 다루지 않을 것이다.

(다만 std::logic_failure와 std::runtime_error가 가지는 의미가 무엇인지 간단하게만 다뤄보도록 하겠다.)

 

 std::logic_failure는 프로그래머의 실수로 발생하는 즉, 프로그래머가 예방할 수 있는 예외들을 의미한다.

예를 들어, 총 3개의 원소를 가진 vector 컨테이너에서 4번째 인덱스를 접근하는 것은, 프로그래머의 실수에 의해 발생하며, 우리가 간단한 코드 수정을 통해 방지할 수 있는 요소이다. (std::out_of_range)

 

 반면, std::runtime_error는 프로그래머가 예방하지 못하는 불가항력적인 예외들을 의미한다.

예를 들어, 내가 돌리고 있는 프로그램이 너무 많은 Memory를 점유하여, 더 이상 Memory 할당이 불가능한 경우가 이에 해당한다.

이런 경우, 프로그래머가 이 예외를 해결하기 위해 할 수 있는 직접적인 방법은 존재하지 않는다.

 

std::exception은 다음과 같이 구성되어 있다.

  1. 기본 생성자
     예외로 std::logic_failure, std::runtime_error매개변수 생성자만을 가지며,
    이때 필요한 인자는 what()을 통해 반환할 문자열이다.

  2. 복사 생성자
     예외가 발생했을 경우, 생성된 예외 객체를 다른 Memory 영역에 복사한다. 그렇기 때문에
    복사 생성자는 반드시 필요하다. 이는 아래에 Exception Mechanism에 관해서 다룰 때 더 자세하게 다를 것이다.

  3. 할당 연산자

  4. virtual what()
     해당 예외 객체가 가지고 있는 에러 메시지를 반환하는 함수이다. 보통 const char* 혹은 std::string 타입을 반환한다.
    가상함수로 하위 클래스에서 overriding가 가능하다. 이는 나만의 exception 클래스를 구현할 때 활용할 수 있다.
  5. virtual 소멸자
    기본적인 클래스 개념을 알고 있다면 왜 상속 구조에서 가상 소멸자를 사용해야 하는지 알 수 있을 것이다.

 

try - catch Expressions & throw

 우리는 이제 표준 예외 클래스인 std::exception에 관해 알게 되었다. 그렇다면 이를 어떻게 활용하는가?

바로 try-catch 문이다.

 

문법은 매우 쉽고 간단하다.

try 블록에 잠재적 예외 발생 코드를 작성하고,

catch 블록에는 try 블록에서 발생한 Error Condition을 인자로 넘겨주고, 이를 기반으로 Handler 코드를 작성하면 된다.

 

아래 코드를 확인하면 이해가 될 것이다.

std::vector<int> v {1,2,3};

try // Try Block: Exception Code
{
	auto elm = v.at(5);
}
catch(const std::exception& e) // Catch Block: Exception Handling
{
	std::cout << e.what() << "\n" // std::out_of_range
}


// nested version
try
{
	try
    {
    	std::cout << "nested-try-catch-block\n";
    }
    catch(...){ ...}
}
catch(const std::exception e) { ... }

 

 위 코드에서 try 블록에선 v의 최대 인덱스를 넘어가는 값에 접근하는 코드를 작성했다. 이 경우 std::out_of_range라는 std::exception의 하위 예외 클래스 객체가 생성된다. 이렇게 발생된 Exception 객체는 catch 블록에 전달된다.

 

이때 핵심 포인트는 std::exception을 참조 형태로 전달하는 것인데, 그 이유는 std::exception의 동적 바인딩을 위해서다.

 

그리고 다중 catch문을 통해 분기별로 여러 예외 객체를 처리할 수 있다. 

std::vector<int> v {1,2,3};


try
{
	auto elm = v.at(10);
}
catch(const std::out_of_range& oor)
{
	std::cout << "Exception: out-of-range\n";
}
catch(const std::domain_error) 
{
	std::cout << "Exception: domain-error\n";
}
catch(const std::exception& e) // 가장 Generic한 Exception 클래스
{
	std::cout << "Exception: generic exception\n";
}

 

이때 핵심 포인트는 아래로 향할수록 Exception 객체가 더 Generic 해야 한다는 것이다. (하위 예외 클래스 => 상위 예외 클래스)

위의 코드의 경우 std::exception의 하위 예외 객체인 std::out_of_range를 전달받는 catch 절로 분기가 이동된다.

 

 마지막으로 Catch-All 문이 존재하는데, 이를 설명하기 앞서 throw 키워드에 관해서 알고 넘어가자.

Error Condition은 프로그램으로부터 자동으로 생성될 수도 있고, 원하는 시점에 직접 생성할 수 있다.

이렇게 의도적으로 예외(객체)를 발생시키는 행위를 예외를 던진다라고 표현한다.

 

이를 가능하게 해주는 것이 throw 키워드이다. 사용법은 throw Error Condition의 형태로 작성하면 된다.

throw std::exception();
throw 42;
throw "hi";
// ...

 

 위 코드를 보면 표준 예외 클래스인 std::exception부터 시작해 정수값과 문자열값도 throwing 하는 것을 볼 수 있다.

 

 앞서 언급했듯 예외를 통해 던지는 객체에는 따로 규격이 존재하지 않는다. 다만 C++11부턴 보다 일관된 개발 환경을 제공하기 위해 위해 표준 예외 클래스를 제공할 뿐이다. 이런 경우, 일반적인 코드로는 발생되지 않고 개발자가 직접 예외를 던져 발생시킨다. 

이는 그다지 권장되지 않는 방법이다.

 

 이렇듯, 표준 예외 클래스로 처리할 수 없거나 미처 고려하지 못한 Error Condition을 catch-all 문을 활용해 일괄적으로 

처리할 수 있다. 문법은 간단하다 catch의 인자로... 을 주면 된다. if-문으로 치면 else와 유사하다.

try
{
	throw 42;
}
catch(std::exception& e) 
{
  // exception handling code ...
}
catch(...)
{
	std::cout << "undefined-exception..\n";
}


// 아래 처럼 처리할 수 있지만 권장되진 않는다.

try
{
	throw 42;
}
catch(int e)
{
	std::cout << "error-code " << e << "\n";
}

 

 하지만 catch-all 문에는 치명적인 단점이 있다. 던져진 예외에 관한 아무런 정보도 전달받을 수 없다는 것이다.

그렇기 때문에 Testing에선 유용할 수 있지만 Debugging 과정에선 활용될 수 없다.

 

Handler Implementation

 예외를 던지기 위한 코드는 별거 없다. 하지만 던져진 예외를 처리하는 것은 주의가 필요하다.

예외 처리를 위한 프로세서를 보통 Handler라고 일컫는다. 이를 우리는 보편적으로 catch 블록 안에 작성해 놓는데,

이때 다음 항목을 고려하여 작성해야 한다.

 

  try 블록에 실행되던 코드에 에러가 발생되어, catch 블록으로 이동을 수행했다는 것은, 우리가 구현한 프로그램이 정상적인 플로우로 진행되고 있지 않음을 의미한다. 이는 현재 프로그램의 상태가 불안정하다(Unstable State)는 것을 의미한다.

이런 상황에서, 아래와 같은 동작을 수행하는 것은 위험한 행동이다.

 

  • 메모리 할당(Memory Allocation)
  • 변수 생성 (Create new Variables)
  • 함수 호출 (Function Call)
  • 새로운 예외 던짐 (New Exception Throwing)

 catch 블록 안의 코드는 매우 간결한 상태(Light Weight)를 유지해야 하며, 불가피하게 변수 생성이 요구된다면, Built-In 타입을 활용하는 것이 적절한 행동이라고 볼 수 있다 E

std::exception과 다형성: 커스텀 Exception Class

 표준 예외 클래스 외에도, 나만의 예외 클래스를 직접 구현할 수 있다. 그렇다면, 커스텀 예외 클래스를 구현하는 과정에서

주의해야 할 점은 어떤 것이 있을까?

 

  • 보편적으로 std::exception 보다는 std::exception의 하위 클래스의 예외를 상속받는 경우가 많다.
    • std::exception는 기본 생성자만 제공되기 때문이다.
    • 이는 나만의 커스텀한 에러 메시지를 추가적으로 담을 수 없음을 의미한다. 
  • 최대한 불필요한 작업을 줄이고 가볍게 디자인하라.
    • 최소한의 동작과 최소한의 데이터 멤버를 지니도록 해야 한다.
    • 이는 프로그램의 불안정한 동작을 방지하기 위함이다.
  • 가장 중요한 점으로 예외 안에서 또 다른 예외를 던지는 행위는 절대로 해선 안된다.

아래는 커스텀 예외 클래스의 예시 코드이다.

class CustomExcpetion : public std::out_of_range // std::exception의 subclass를 상속
{
	string _errmsg; // C++ String
    char* _cerrmsg; // C-String
 public:
	CustomException(const char* msg) : _errmsg(msg), _cerrmsg(std::move(msg)) {}
    
    const char* what() override 
    { 
    	// ... add extra information
    	if(!_errmsg.empty()) return _errmsg.c_str();
        return cerrmsg;
    }
    
    CustomExcpetion& operator=(const CustomException& rhs) 
    {
    	if(&rhs != this)
        {
    	    _errmsg = rhs._errmsg;
            if(_cerrmsg != nullptr) delete _cerrmsg;
            auto size = rhs._errmsg.length();
            _cerrmsg = new char[size];
       	    for(auto i = 0; i < size; ++i ) _cerrmsg[i] = rhs._cerrmsg[i];
        }
        return *this;
    }
    ~CustomException() {}
}

Exception Mechanism (Feat. Stack Unwinding)

그렇다면 try-catch 블록이 동작되는 메커니즘에 관해 설명해 보겠다.

  1.  임의의 코드에서 Exception Throwing 발생 & Exception 객체 생성
  2.  생성된 Exception 객체는 Compiler가 Set-up 한 메모리에 복사할당된다.
  3.  Catch Block에서 저장된 Exception 객체에 Access
  4.  기존 Try 블록에 존재한 모든 변수 & 객체들이 소멸
  5.  예외 코드 이후 동작할 모든 구문 수행을 멈추고 Scope를 탈출
  6.  만약 Catch 블록에 적절한 Handler가 없을 경우, 다음 상위 Scope 영역에서 탐색 수행(반복)

위 6 단계의 절차를 통해 진행된다. 그림으로 표현하면 아래와 같다.

 

Exception Mechanism

 6번 과정으로 인해 지속적으로 Scope를 탈출해 나가며 만약 Main 함수까지 도달하게 된다면, 프로그램은 terminate 된다.

이러한 일련의 과정을 Stack Unwinding이라고 일컫는다. (로컬 Stack을 파괴해 나가며 거슬러 올라간다.)

컴파일러는 Stack Unwinding을 수행하기 위한 Extra Code들을 컴파일 과정에서 추가한다.

 

 이때 기존에 발생한 동일한 예외를 일부로 다시 던지는 방법도 존재하는데, 이를 Rethrowing Exception이라고 한다.

Catch 블록에서 throw;를 선언하면 된다. 이를 수행하는 이유는 다음과 같다.

  • 현재 Catch 블록이 아닌 다른 곳에서 Error를 핸들링하고 싶을 경우
    • 현재 Catch 블록에서 추가적인 예외 정보만 추가해서 전달한다.
  • Low Level의 Exception 클래스를 High Level의 Exception 클래스로 Convert 하기 위한 경우
    • Ex) CustomException → std::out_of_range

Exception Safe

 코드를 작성하고, 이에 관한 예외를 검증하는 과정에서, 던져진 예외가 나의 의도에 맞게 동작하는 것을 Exception Safe라고 설명한다. 이 Exception Safe의 경우, 내가 코드를 작성하는 과정에서 어떤 요소를 만족하느냐에 따라 Exception Safe의 보장 정도가 달라진다. 크게 다음 3 가지로 분류된다.

  1. Basic Excpetion Guarantee: 가장 기본적인 Exception Safe를 보장하기 위한 규칙.
    • 연산 과정에서 던져진 예외에 대해 메모리 누수가 발생하지 않는다.
    • File 객체를 Open 하는 것에 실패했을 경우, File 객체는 닫힌 상태를 만족해야 한다.
    • 메모리 할당 실패 시, 해당 메모리는 반드시 Release 돼야 한다.
  2. Strong Exception Guarantee: 강력한 Exception Safe를 보장하기 위한 규칙. Basic 보다 더 빡빡한 규칙을 제시한다.
    • 예외가 던져졌을 경우, 프로그램의 상태를 Revert 시킨다. (이와 관련된 모든 요소들은 정상상태를 유지한다.)
    • Database의 트랜잭션의 규칙과 유사하다. Commit 되기 전에 문제가 발생했을 경우, 아무 일도 일어나지 않은
      상태로 되돌린다. (Rollback)
  3. No Throw guarantee: 예외를 발생시키지 않음으로써 Exception Safe를 보장한다.

throw()(C++98) VS noexcept(C++11)

 그렇다면 exception과 관련된 새로운 기능이 지원되기 이전에는 예외를 어떤 형태로 처리했는가? 

바로 throw()를 사용하는 것이다. 해당 throw()는 앞서 언급했던 throw 키워드와는 완전히 다른 목적을 수행하는 키워드이다. throw()의 주된 목적은 throw처럼 예외를 던지는 것이 아닌, 해당 함수에서 발생할 수 있는 예외에 관한 정보를 개발자에게 명시하는 목적이다. 그리고 예외 발생 시, 명시된 Error Condition들을 반환하도록 한다.

 

 주로 예외를 발생시킬 수 있는 함수에 관해 해당 함수의 선언 뒤에 throw() 키워드를 사용했다. 문법은 다음과 같다.

() 안에 해당 함수가 발생시킬 수 있는 모든 Error Condition에 관해 명시하며, 명시된 Error Condition 외의 예외가 발생했을 경우, 프로그램을 Terminate 한다.

void func() throw(ErrorCode, ExceptionObject, ...) // () 안에 해당 함수에서 발생할 수 있는 
{                                                    // 모든 Error Condition을 명시한다. 
 // ... do something
}


// C++11

void func() noexcept
{
}

 

 하지만 이 키워드의 가장 큰 문제점은 컴파일러에 의해 검증이 불가능하다는 것이다. void func()에서 발생할 수 있는 모든 Error Condition에 관해 명시했다고 하더라도, 코드의 리팩토링 과정에서 새로운 예외가 발생할 수 있다. 하지만 이 부분은 개발자가 스스로 파악하기 어렵다. 이 경우, 아무 영문도 모른 채 프로그램이 뻗어버리는 것을 지켜볼 수밖에 없는 것이다.

 

 이러한 치명적인 단점에 의해 throw()는 C++17 이후로 완전히 삭제된 문법으로 사용을 지양하는 것이 좋다.

심지어, Catch-All 문이 있기 때문에 예상하지 못한 예외도 직접 확인할 수 있는 수단이 존재한다.

 

 그 대신 C++11부터 noexcept라는 키워드를 사용할 수 있다.

 noexcept 키워드는 해당 키워드의 명칭에서도 알 수 있듯이 예외가 발생하지 않음을 명시적으로 나타낸 것이다.

이는 No Exception guarantee를 만족하기 때문에 Exception Safe 하다고 볼 수 있다.

 

 그뿐만 아니라, noexcept는 컴파일러로 하여금 추가적인 정보를 전달하기 때문에 퍼포먼스적 이점도 제공한다. 

Exception Mechanism 과정에서 컴파일러가 Stack Unwinding을 위한 Extra 코드를 작성해 준다고 했다. 이때

noexcept 키워드가 붙은 경우, 해당 Extra 코드를 추가하지 않기 때문에 컴파일 시간의 절약이 가능하다.

 

 이러한 이점을 활용해, 일부 Modern C++ Library 일부 함수에선 해당 키워드를 기반으로 최적화하여 동작하는 연산을

구현했다(Copy대신 Move 수행). 참고로 std::swap은 대표적인 noexcept 키워드 함수이다. 

 

noexcept의 추가적인 특성을 정리하면 다음과 같다.

 

  • noexcept 키워드는 함수의 시그니처(argument, function name)라기보단, 특성을 부여하는 것이라고 보면 된다.
  • noexcept는 상속도 가능하다. noexcept 가상 함수를 오버라이딩 할 때, 반드시 noexcept 키워드를 붙여줘야 한다.
    • 이때 내가 작성을 까먹더라도 컴파일러가 알아서 해당 키워드를 Synthesize 해준다.
  • 상속 구조 설계 과정에서 no exception safe 한 클래스를 exception safe 하게 구체화하는 것은 가능하나, 그 역은
    불가능하다.
  • C++11부턴 모든 소멸자에 noexcept 키워드가 알아서 포함된다. (아래 항목에서 이어서 설명)

Class Member Function과 noexcept

 클래스에는 특별한 멤버함수인 생성자와 소멸자가 존재한다. 이러한 특수한 멤버 함수에서 예외가 발생하면 어떻게 될까?

 

생성자

 생성자는 간단하다. 생성자에서 예외가 발생하면 해당 객체를 생성하지 않으면 된다. 동적할당을 수행했을 경우, 해당

메모리는 바로 Release 되며 이를 받는 포인터 변수는 nullptr를 가리킨다. (부분적으로 생성하는 것은 일어나지 않음.)

이는 상속 구조를 이루는 하위 클래스에서도 마찬가지다.

 

  이때 생성자에서 발생한 예외는 반드시 생성을 수행한 호출자가 다루도록 해야 한다. 그래야 생성 여부를 파악할 수 있기 때문이다. 예외로 로깅을 위해서 내부에서 일부로 예외를 던지는 경우도 있다.

 

 소멸자

  하지만 소멸자에서 예외가 발생한다면 어떤 현상이 발생할까?

만약 소멸자에서 나의 의도에 맞게 Try-Catch문을 작성했다면 문제 되지 않는다. 하지만 이를 만족하지 않을 경우 심각한 문제가 발생한다. 그 이유는 Stack Unwinding 동작과 관련이 된다.

 

일단 어떤 예외가 발생했다고 가정해 보자 그렇다면 다음 flow를 따라갈 것이다.

  1. 예외 발생
  2. Stack Unwinding 수행 → 모든 변수와 클래스는 소멸되며 이때 소멸자가 호출됨.
  3. 소멸자에서 예외 발생 
  4. 3번 과정에 대한 Stack Unwinding 수행 → 동일한 Stack 공간 내에서 2번의 Stack Unwinding이 수행되게 된다.

 이는 정의되지 않은 행동(Undefined Behavior)으로 이어진다.

그렇기 때문에 소멸자는 소멸자 내부에서 예외를 핸들링할 수 있지 않는 이상, 절대로 예외를 던지지 않는다.

noexcept와 std::swap() overloading

마지막으로 std::swap 함수에 관한 오버로딩 방법을 소개하고 마무리 짓도록 하겠다.

std::swap함수는 Move를 지원하기 때문에, 크기가 큰 클래스들을 효율적으로 교환하기 위해 유용하게 활용되는

함수이다. 아래와 같은 방식으로 구현하면 std::swap함수를 통해 클래스의 효율적인 교환이 가능하다.

class Test
{
 int i;
 char c;
 double d;
public:
Test() = default;
~Test() = defualt;
friend void swap(Test& a, Test& b) noexcept;
}


inline void swap(Test& a, Test& b) noexcept
{
  using std::swap;
  swap(a.i,b.i);
  swap(a.c,b.c);
  swap(a.d,b.d);
}