C++/C++

noexcept as specifier & operator

Elan 2021. 3. 19. 23:07

noexcept as specifier


 

함수(메소드, 람다 포함)에 noexcept를 붙여 선언하는 것은 예외를 발생시키지 않겠다는 것을 명시하는 것이다.

다시 말해 해당 함수에서의 예외를 허용하지 않겠다고 명시하는 것이다.

 

따라서 컴파일러는 해당 함수에서의 예외 발생을 염두하지 않고 컴파일해버린다.

만약 해당 함수에서 예외가 발생하게 되면 예외가 제대로 처리되지 않고 프로그램이 종료된다.

 

참고로 C++11 부터 소멸자들은 기본적으로 noexcept이다.

따라서 절대로 소멸자에서 예외를 던지면 안된다.

 

noexcept 에 전달된 expression이 true로 평가될 경우 noexcept 지시어는 유효하다.

하지만 false일 경우 noexcept는 무효가 되므로 예외를 던질 가능성을 가지게된다.

void func1() noexcept;        // does not throw
void func2() noexcept(true);  // does not throw
void func3() throw();         // does not throw : deprecated with C++11 and will be removed with C++20
//위의 3가지는 모두 같은 표현이다

void func4() noexcept(false); // may throw

 

 

noexcept를 명시하면 다음과 같은 특징이 있다.

 

첫째,

noexcept 가 명시된 함수는 다른 noexcept 가 명시된 함수에서 안전하게 사용될 수 있다.

 

둘째,

컴파일러는 noexcept 가 명시된 함수에서 예외가 발생하더라도

std::unexpected를 호출하지 않으며, Stack Unwinding(스택 풀기) 하지 않겠다는 의미로 받아들인다.

따라서 컴파일러에게는 최적화 기회이다.

 

셋째,

만약 이동 생성자가 noexcept로 선언되지 않을 경우

메모리를 동적으로 관리하는 std::vector 같은 컨테이너에서 해당 이동생성자를 사용할 수 없게 되어 복사 생성자가 대신 호출된다.

그 이유는 다음과 같다.

메모리 부족으로 인하여  Memory Re-allocation 후 복사하는 과정에서 예외가 발생했다고 가정해보자.

Re-allocation된 메모리를 해제해버려도 기존의 데이터들은 남아있어서 예외를 처리하기만 하면 된다.

하지만 복사가 아닌 이동의 경우 이미 기존의 데이터를 새로운 메모리 영역에 이동시켜버렸고,

이때 예외 발생으로 인해 새로운 메모리를 해제시킬 경우 데이터를 완전히 잃어버리게 된다.때문에 컴파일러가 이동이 아닌 복사하는 방식을 선택한다.

결과적으로 이동생성자에 noexcept가 명시되지 않았다면 비용이 높은 copy가 일어나게 될 것이다.

 

 

Potentially throwing (잠재적으로 throw 가능성이 있는 경우)

- 함수가 throw하는 함수를 사용 할 경우

- 함수가 noexcept 로 선언되지 않은 경우

- 함수가 dynamic_cast를 참조 타입에 사용하는 경우

 

함수가 noexcept로 선언되지 않은 경우에 예외를 던질 수 있는 특수 멤버가 6가지 있다.

- 기본 생성자

- 기본 소멸자

- 이동 생성자

- 복사 생성자

- 이동 대입 연산자

- 복사 대입 연산자

 

위의 6가지 멤버 메소드는 non-throwing이 될 수 있는 조건이 있다.

바로 해당 클래스의 소멸자와 base 클래스의 소멸자가 모두 non-throwing일 경우이다.

 

만약 non-throwing 으로 선언된 함수에서 예외가 생길 경우 std::terminate가 호출된다.

std::filename은 현재 설치된 std::terminate_handler를 호출하며 핸들러가 std::abort를 기본적으로 호출된다.

결국 프로그램은 비정상적으로 종료될 것이다.

 

 


noexcept as operator


noexcept 연산자는 컴파일 시 expression이 예외를 던지는지 확인한다.

noexcept 연산자가 expression을 평가하는 것은 아니다.

 

함수가 현재 타입에 따라 예외를 던질 수 있음을 선언하기 위해 함수 템플릿에서 noexcept specifier(예외 지정자)로 사용할 수 있다.

#include <iostream>
#include <array>
#include <vector>

class NoexceptCopy{
public:
  std::array<int, 5> arr{1, 2, 3, 4, 5};             // (2)
};

class NonNoexceptCopy{
public:
  std::vector<int> v{1, 2, 3, 4 , 5};                // (3)
};

template <typename T> 
T copy(T const& src) noexcept(noexcept(T(src))){     // (1)
  return src; 
}

int main(){
    
    NoexceptCopy noexceptCopy;
    NonNoexceptCopy nonNoexceptCopy;
    
    std::cout << std::boolalpha << std::endl; // bool 자료형에 대한 출력을 0,1 이 아닌 true/false로 변경
    
    std::cout << "noexcept(copy(noexceptCopy)): " <<            // (4)
                  noexcept(copy(noexceptCopy)) << std::endl;    // true
                   
    std::cout << "noexcept(copy(nonNoexceptCopy)): " <<         // (5)
                  noexcept(copy(nonNoexceptCopy)) << std::endl; // false

    std::cout << std::endl;

}

(1) 에서 바깥쪽 noexcept는 specifier 이며 안쪽의 noexcept는 operator이다.

noexcept (T(src) ) 이라는 expression 은 T 라는 타입의 copy constructor 가 non-throwing인지 확인하는 역할을 한다.

 

(4) 의 expression인 noexcept( copy ( noexceptCopy ) ) 는 true를 반환한다. 

이유는 (2)에서 클래스 NoexceptCopy 의 멤버인 std::array는 예외를 throw하지 않기 때문이다.

 

(5) 의 expression인 noexcept( copy ( nonNoexceptCopy ) ) 는 false를 반환한다. 

std::vector의 예외를 throw할 가능성이 있는 copy constructor 때문이다.

type traits library를 이용하여 non-throwng 확인 방법

template <typename T> 
T copy(T const& src) noexcept(std::is_nothrow_copy_constructible<T>::value){
  return src; 
}

 


C++ 예외 발생


https://devpp.tistory.com/186