noexcept 지시자( specifier )
noexcept는 함수 이름의 끝에 붙는 지시자로, 이 지시자가 붙은 함수는 예외( exception )를 발생시키지 않는다는 것을 명시합니다.
하지만, noexcept 함수 실행 시, 컴파일러에 의해 예외가 발생하는 것이 방지되거나 해서, 프로그램이 예외로부터 안전해지는 것을 말하는 것이 아니라, 이 함수에서는 예외가 발생하지 않으니까, 예외에 민감한 문맥에서도 안심하고 사용할 수 있을 뿐만 아니라, 컴파일러는 이 함수를 최적화해서 성능을 향상할 수 있다고 얘기를 하는 것입니다.
즉, 이 noexcept 지시자를 사용한다는 것은, 함수와 이 함수를 사용한 프로그램 간의 일종의 계약이라고 봐야 합니다.
그런데 만약, 이렇게 말해온 함수에서 예외가 외부로 발생하면 어떻게 될까요?
그것은 계약 파기죠.
프로그램은 이러한 문제에 대해, std::terminate 함수를 호출하여, 실행을 중단하는 것으로 단호하게 대처합니다.
따라서, noexcept 함수는 함수 내에서 모든 예외( exception )를 처리해야 합니다.
[C++] 예외 처리를 위한 throw와 try-catch의 동작 방식
C++에서의 예외( exception ) 처리C++에서는 프로그램의 오류나 기대하지 못했던 상황이 발생한 경우, 이를 처리하기 위한 예외( exception )를 발생시키면, 프로그램이 자동으로, 이 예외를 처리할 수
codingbonfire.tistory.com
만약, 예외가 noexcept 함수 밖으로 나가면( propagation ), 이 예외를 처리할 수 있는 코드가 있더라도 프로그램은 중단됩니다.
다음 예제에서 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(...){ // catch-all 처리기
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 함수에서 예외를 발생시켰습니다.
하지만, 이 예외를 call_function 함수 내에서 처리했으므로, 프로그램이 종료되지 않습니다.
그렇지만, noexcept 함수인 call_noexcept_throw_func은 함수 내에서 예외를 처리하지 못했습니다.
그래서, 이 함수를 호출한 call_function 함수가 이 예외를 처리할 수 있음에도 불구하고, 프로그램은 중단됩니다.
따라서, noexcept 함수는 이 함수가 호출하는 다른 함수도 예외를 발생하는지, 그렇다면 이 발생된 예외를 처리할 수 있는지에 주의해야 합니다.
● 참고로, 지시자( specifier )는 선언( declaration )의 속성을 변경하는 키워드를 말합니다.
이러한 지시자의 예로, 멤버 함수의 뒷부분에 붙은 const 키워드를 들 수 있습니다.
그런데, 이 const 멤버 함수는 const 지사자의 차이로 오버로딩( overloading ) 될 수 있습니다.
class CSomething{
void memberFunc() const {}; // overloading
void memberFunc() {};
};
하지만, noexcept 함수는, 이러한 지시자의 차이로 오버로딩 될 수는 없습니다.
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
● 참고로, 위의 std::boolalpha는 값을 가진 변수가 아니라, 함수입니다.
이 함수가 ( std::cout
클래스의 ) <<
연산자의 피연산자로 사용되어 호출될 때, 이 함수에 전달된 std::cout
객체의 내부 플래그( flags )를 설정합니다.
그리고, 이 플래그가 설정된 이후, 출력 스트림에 입력되는 bool
값은 숫자가 아니라 "true" 또는 "false" 문자열로 변환되어 출력됩니다. ( 0
이 아니면 "true"로 출력 )
그리고, 이 boolalpha 함수는 삽입 연산자 <<
의 연쇄 함수 호출이 가능하도록, 전달된 스트림 객체를 다시 반환하도록 구현되어 있습니다.
암시적으로 noexcept 함수들
다음의 몇 가지 함수들은 암시적으로 noexcept 함수입니다.
- 클래스의 소멸자( destructor )
- 암시적으로 생성된 복사 생성자( copy constructor ), 이동 생성자( move constructor ), 그리고 기본 생성자( default constructor )
- 암시적으로 생성된 복사 대입연산자( copy assignment ), 이동 연산자( move assignment )
● 아래는 클래스의 소멸자( destructor )가 ( noexcept 지시자를 사용하지 않더라도 ) noexcept 함수인지를 보여주고 있습니다.
class CSomething{
public:
~CSomething(){} // 소멸자
};
int main(){
CSomething obj;
std::cout << boolalpha << noexcept( obj.~CSomething() ) << '\n';
}
▼출력
true
만약, 위의 소멸자가 예외가 발생시킬 수 있다고 명시적으로 선언하려면, 다음과 같이 noexcept 지사자에 false
매개 변수를 사용할 수 있습니다.
( 이와 반대로 noexcept(true)
를 사용할 수도 있는데, 이것은 noexcept를 사용하는 것과 동일합니다. )
~CSomething() noexcept(false) { // 명시적 선언
// do something...
}
하지만, 클래스의 소멸자에서 예외를 외부에 전파하면 프로그램이 불안정하게 될 수 있습니다.
그래서, 소멸자를 암시적으로 noexcept 함수로 지정한 것이고, 이 함수 내에서 예외가 발생하지 않도록 주의하는 것이 좋습니다.
[C++] 생성자와 소멸자에서의 예외( exception ) 처리
생성자( constructor )에서의 예외 처리생성자를 작성하다 보면, 의외로 문제가 발생할 여지가 꽤 있다는 것을 깨닫게 됩니다. 맨 먼저 떠오르는 경우는, 메모리를 대규모로 할당할 때, 실패
codingbonfire.tistory.com
● 또한, 위의 함수들은, 함수 내부에서 예외를 발생할 수 있는 함수를 호출하게 되면, noexcept 함수로 인정받을 수 없습니다.
class CChild{
public:
CChild(){}; // noexcept 함수가 아님
};
class CSomething{
CChild m_Data;
};
int main(){
cout << boolalpha << noexcept(CSomething()) << endl;
}
▼출력
false
위의 CSomething
의 암시적인 기본 생성자( default constructor )는 기본적으로 noexcept입니다.
하지만, 이 클래스의 CChild
객체를 생성할 때 호출하는 기본 생성자가 noexcept 함수가 아니므로, 이 생성자는 더 이상 noexcept 함수가 아닙니다.
noexcept 지시자를 사용하는 이유
noexcept 함수는 예외가 발생하는 것을 방지하는 것이 아니라, 예외가 발생하지 않는다고 말하는 것일 뿐입니다.
그럼, 단지 예외가 발생하는지 않는다고 말하는 것으로 얻을 수 있는 이득은 무엇일까요?
● 첫 번째, 예외( exception )를 발생하지 않는다고 표명하는 함수는, 클래스의 소멸자( destructor )같이, 예외에 취약한 함수에서 부담 없이 호출할 수 있습니다.
만약, 소멸자에서 예외가 발생해 외부로 전파되면, 이 결과를 예측할 수 없게 됩니다.
그래서, 이러한 소멸자는 사용할 함수에 민감할 수밖에 없습니다.
예를 들어, 클래스에서 할당한 메모리를 해제하기 위한 delete와 delete [] 연산자가 예외를 발생할 수 있다고 가정해 봅시다.
그럼, 이 가능성 때문에, 이 연산자를 호출하는 소멸자는, 내부적으로 예외를 처리하기 위해, try-catch 블록을 추가해야 할 것입니다.
( 다행스럽게도, 사실 delete, delete [] 연산자는 암시적으로 noexcept 함수입니다. )
이렇기 때문에, 예외에 취약한 함수들에서 noexcept 함수를 선호하는 것은 당연합니다.
● 두 번째, noexcept 함수는 예외를 외부로 내보내지 않기 때문에, 실시간으로 예외를 추적하기 위해 스택을 유지해야 하는 부담이 없어집니다.
그래서, 컴파일러가 이러한 함수에 대해 최적화를 할 수 있는 가능성이 높아지게 되고, 결과적으로 함수의 성능의 향상됩니다.
따라서, 예외가 발생하지 않는 함수들에 noexcept 지시자를 사용하는 것은 선호됩니다.
● 세 번째, 함수가 noexcept인지 아닌지를 구분할 수 있게 되면, 이에 따라 효율적인 코드를 구현할 수 있습니다.
현재 구현되어 있는 C++ standard library의 컨테이너들이 이것의 예라고 할 수 있는데, 이 컨테이너들은, 위에서 설명한, noexcept 연산자를 사용해, 저장되는 객체의 멤버 함수들이 noexcept 함수인지를 판단합니다.
예를 들어, std::vector
컨테이너는 강력한 예외 안전성 보장( strong exception safety guarantee )을 제공합니다.
이것은, vector
의 기능을 실행하는데 예외가 발생하면, 모든 상태를 예외가 발생하기 이전의 상태로 돌리는 것을 말합니다.
이것을 보장하기 위해서, vector
는 이 객체의 원소가 이동 생성자( move constructor )를 구현했더라도, 이 생성자가 noexcept 함수가 아닌 경우, 복사 생성자( coppy constructor )를 호출합니다.
그 이유는 다음과 같습니다.
예를 들어, vector
의 push_back를 호출한다고 해보죠.
vector<string> vec;
string str("7");
vec.push_back( move(str) );
만약, vector
의 원소의 수가 capacity를 넘기면, 내부 버퍼의 재할당이 일어나고, 할당된 새로운 버퍼에 기존의 객체들을 옮깁니다.
이때, 이 객체들의 클래스가 이동 생성자( move constructor )를 구현했다면, move 연산을 수행하는 것이 당연합니다.
그러나, 이 과정에서 예외가 발생한다면, 이전의 vector
상태로 돌아갈 수 없게 됩니다.
따라서, vector
는 이동 생성자 대신 복사 생성자( copy constructor )를 호출하는 것입니다.
이 복사 과정에서는 예외가 일어나더라도, 단순히 새로 할당된 버퍼를 파괴하고, 함수를 종료하는 것으로 기존의 상태를 유지할 수 있습니다.
기존의 객체들은 전혀 변경된 것이 없습니다.
정말로 그런 과정을 거치는지, 코드를 통해서 확인할 수 있습니다.
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 { // 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는 함수가 예외를 발생하지 않는 것을 명시하는 지시자( specifier )입니다.
- 예외를 발생하지 않는 함수에 noexcept 지시자를 사용하는 것은 이득을 얻을 수 있습니다. 특히, 이동 생성자( move constructor )와 이동 대입 연산자( move assignment operator )는 noexcept 함수로 구현해야 합니다.
- noexcept 함수 내에서 예외를 처리하지 못하면 프로그램이 종료됩니다.