[C++] noexcept 키워드를 사용하는 이유

noexcept 키워드

noexcept는 함수 이름의 끝에 붙는 키워드로, 이 키워드가 붙은 함수는 예외를 발생시키지 않는다는 것을 명시합니다.

 

참고로, noexcept( true )로 쓸 수도 있는데, 이것은 noexcept와 같은 기능을 수행합니다.

이 반대는 noexcept( false )으로, 함수가 예외를 발생시킬 수도 있음을 나타냅니다.

이렇게 선언하는 것은 template을 통한 함수를 정의할 때 사용하기 위한 것이라고 보면 됩니다.

 

하지만, 이 키워드를 사용하면 컴파일러에 의해 예외가 방지되거나 해서, 프로그램이 예외로부터 안전해지는 것을 말하는 것은 아닙니다.

이 키워드는 noexcept를 사용한 함수의 개발자와 그 코드를 사용한 프로그램 간의 일종의 계약이라고 봐야 합니다.

함수 개발자는 이 함수에서는 예외가 발생하지 않으니까, 안심하고 이 함수를 사용해서 성능을 향상할 수 있다고 얘기를 하는 것입니다.

 

그런데, 그렇게 말해온 함수에서 예외가 발생하면 어떻게 될까요?

 

그것은 계약 파기죠. 

프로그램은 이러한 문제에 실행을 중단하는 것으로 단호하게 대처합니다.

따라서, noexcept 함수는 함수 내에서 모든 예외를 처리해야 합니다.

만약, 예외가 함수 밖으로 나가면, 이 예외를 처리할 수 있는 코드가 있더라도 프로그램은 중단됩니다.

 

다음 예제에서 noexcept 키워드를 가진 함수가 예외를 발생시켰을 때의 결과를 볼 수 있습니다.

#define NORMAL_FUNC 1
#define NOEXCEPT_FUNC 2

void call_throw_func(){

    cout << "throw exception 1\n";
    throw(1);
}

// noexcept 함수
void call_noexcept_throw_func() noexcept {

    cout << "throw exception 2\n";
    throw(2);
}

// call_function 자체도 noexcept 함수
void call_function( int func_type) noexcept {

    try{
        if ( func_type == NORMAL_FUNC){
            call_throw_func();
        }
        else{
            call_noexcept_throw_func();
        }
    }
    catch(...){

        cout << "exception is catched\n";
    }

    cout << "type " << func_type << " function was called sucessfully\n\n";
}

int main(){


    call_function(NORMAL_FUNC);   // call normal function

    call_function(NOEXCEPT_FUNC);   // call noexcept funtion

}

▼출력

throw exception 1
exception is catched
type 1 function was called sucessfully

throw exception 2
terminate called after throwing an instance of 'int'

call_function 함수는 noexcept 함수입니다.

그런데, 이 함수가 호출한 call_throw_func 함수에서 예외를 발생시켰습니다.

하지만, 이 예외를 함수 내에서 처리했으므로 프로그램이 종료되지 않습니다.

 

그렇지만, noexcept 함수인 call_noexcept_throw_func은 함수 내에서 예외를 처리하지 못했습니다.

따라서, 프로그램은 중단됩니다.

 

noexcept 연산자

noexcept는 연산자( operator )로서의 기능도 갖고 있습니다.

연산자로서의 noexcept는 컴파일 시에 함수 표현식을 판단해서 noexcept 함수이면 true를, 아닌 경우 false를 반환합니다.

 

하지만, 실제 함수의 코드를 평가하는 것은 아니고, 함수에 noexcept 키워드가 사용되었다면 true를 반환하는 식으로 정적인 판단을 합니다.

// noexcept 함수인지 판단

// 예외를 발생하므로 false
void exception_thrower(){ throw(1); }   

// 예외를 발생할지 판단할 수 없으므로 false
void normal_function(){}

// noexcept가 있으므로 true
void noexception_function() noexcept {}

// 클래스의 기본 생성자는 암시적으로 noexcept. 따라서 true
class default_class{};

int main(){
   
    cout << boolalpha << noexcept(exception_thrower()) << endl;
    cout << boolalpha << noexcept(normal_function()) << endl;
    cout << boolalpha << noexcept(noexception_function()) << endl;
    cout << boolalpha << noexcept(default_class()) << endl;
}

▼출력

false
false
true
true

 

다음의 몇 가지 함수들은 암시적으로 noexcept 함수입니다.

  • 클래스의 소멸자( destructor )
  • 암시적으로 생성된 생성자와 기본 생성자
  • 암시적으로 생성된 대입연산자

참고로, 위의 boolalpha는 bool 타입을 문자열로 출력하는 함수입니다.

만약, 값이 1이면 "true"를 출력합니다.

 

그런데, 왜 noexcept 키워드를 사용할까 하는 의문이 생깁니다.

단지, 예외가 발생하는지 않는다고 말하는 것으로 어떤 이득을 얻을 수 있는 것일까요?

 

noexcept 키워드를 사용하는 이유

noexcept 키워드를 사용함으로써 다음과 같은 이득을 얻을 수 있습니다.

 

첫 번째, noexcept를 표시한 사용한 함수는 클래스의 소멸자( destructor )같이 예외에 취약한 함수에서 부담 없이 호출할 수 있습니다.

소멸자에서 예외가 발생해서, 외부로 예외가 나가게 되면 결과가 정의되지 않습니다.

그래서, 소멸자와 메모리를 반환하는 함수 delete, delete []는 암시적으로 noexcept 함수입니다.

이렇게 취약한 함수들에서 noexcept 함수를 선호하는 것은 당연합니다.

 

두 번째, noexcept 함수는 예외를 외부로 내보내지 않기 때문에, 예외를 실시간으로 추적하기 위해 스택을 유지해야 하는 부담이 없어집니다.

이러한 컴파일러는 이 함수에 대해 최적화를 할 수 있게 되고, 결과적으로 함수의 성능의 향상됩니다.

따라서, 예외가 발생할지 않는 함수들에 noexcept 키워드를 사용해 최적화할 수 있습니다.

 

세 번째, 함수가 noexcept인지 아닌지를 구분할 수 있게 되면, 이에 따라 효율적인 코드를 구현할 수 있습니다.

현재 구현되어 있는 C++ standard library의 컨테이너가 이것의 예라고 할 수 있습니다.

이 컨테이너들은 이러한 구분을 위해, 위에서 설명한 noexcept 연산자를 사용해 함수가 noexcept 함수인지 판단합니다.

 

예를 들어, vector 컨테이너는 강력한 예외 안전성 보장( strong exception safety guarantee )을 제공합니다.

이것은, vector의 기능을 수행하는데, 예외가 발생하면 모든 상태를 예외가 발생하기 이전의 상태로 돌리는 것을 말합니다.

 

이것을 보장하기 위해서, vector는 vector의 원소가 이동 생성자( move constructor )를 구현했더라도 이 생성자가 noexcept 함수가 아닌 경우, 복사 생성자( coppy constructor )를 호출합니다.

 

이동 생성자에 관한 내용은 여기에서 볼 수 있습니다.

 

[C++] 우측값 참조( rvalue reference )와 이동 생성자( move constructor )

우측값 참조( rvalue reference )우측값 참조란 우측값을 참조하는 데이터 타입을 말합니다.이 글에서는 우측값 참조를 왜 만들었으며, 이 참조를 사용하는 법에 대하여 설명하겠습니다. 먼저, 시간

codingembers.co.kr

 

그 이유는 다음과 같습니다.

예를 들어, vector의 push_back를 호출한다고 해보죠.

vector<string> vec;
string str("7");
vec.push_back( move(str) );

 

만약, vector의 원소의 수가 capacity를 넘기면 내부 버퍼의 재할당이 일어나고, 할당된 새로운 버퍼에 기존의 객체들을 옮깁니다.

이때, 객체의 클래스가 이동 생성자를 구현했다면, 당연히 move 연산을 수행해야 합니다.

그러나, 그 과정에서 예외가 발생하면, 기존의 백터 상태로 돌아갈 수 없게 됩니다.

 

이동 연산을 통한 버퍼 확장

 

따라서, vector는 이동 생성자대신 복사 생성자를 호출하는 것입니다.

이 복사 과정은 예외가 일어나더라도, 단순히 새로 할당된 버퍼를 파괴하고, 함수를 종료하는 것으로 기존의 상태를 유지할 수 있습니다.

기존의 객체들은 전혀 변경된 것이 없습니다.

 

복사를 통한 버퍼 확장

 

정말로 그런 과정을 거치는지, 코드를 통해서 확인할 수 있습니다.

class TestObj{
    int m_val;
public:

    TestObj(int val) : m_val(val){};

    TestObj( const TestObj& other){	// 복사 생성자
        m_val = other.m_val;
        cout << m_val << ": Copy constructor\n";
    }

    TestObj( TestObj&& other) {	// 이동 생성자
        m_val = other.m_val;
        cout << m_val << ": Move constructor\n";
    }
};

int main(){

    vector<TestObj> vec;
    const int cnt = 2;

    for( int i = 1; i <= cnt; i++){
        TestObj obj(i);
        vec.push_back( move(obj) );
    }

    int cap = vec.capacity();
    cout << "current capacity: " << cap << endl;
}

▼출력

1: Move constructor
2: Move constructor
1: Copy constructor
current capacity: 2

위의 TestObj의 이동 생성자는 noexcept가 아닙니다.

따라서, vector의 내부 버퍼를 재할당 시, 복사 연산을 수행합니다.

 

이번엔, 같은 코드를 noexcept 이동 생성자에 대하여 실행하면 다음과 같은 결과를 볼 수 있습니다.

class TestObj{
    int m_val;
public:

    TestObj(int val) : m_val(val){};

    TestObj( const TestObj& other){ // 복사 생성자
        m_val = other.m_val;
        cout << m_val << ": Copy constructor\n";
    }

    TestObj( TestObj&& other) noexcept {    // 이동 생성자
        m_val = other.m_val;
        cout << m_val << ": Move constructor\n";
    }
};

▼출력

1: Move constructor
2: Move constructor
1: Move constructor
current capacity: 2

 

따라서, 이동 문법( move semantics )을 통해 성능 향상 꾀하고자 한다면 이동 생성자( move constructor )와 이동 대입 연산자( move assignment operator )를 noexcept 함수로 구현해야 합니다.

 

그리고, 실제로 대부분 데이터를 swap 하는 방식으로 구현하는 이동 생성자나 이동 대입 연산자를 예외를 발생하지 않는 함수로 작성하는 것은 그렇게 힘든 작업은 아닙니다.

 

정리

  • noexcept는 함수가 예외를 발생하지 않는 것을 명시하는 키워드입니다.
  • noexcept 키워드를 사용함으로써 이득을 얻을 수 있습니다.
  • 특히, 이동 생성자와 이동 대입 연산자는 noexcept 함수로 구현해야 합니다.
  • noexcept 함수 내에서 예외를 처리하지 못하면 프로그램이 종료됩니다.

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유