noexcept 지정자

noexcept 지정자 (C++11)

함수가 예외를 던질 수 있는 함수인지 아닌지를 함수 선언에 명시하고 싶을 때가 있어요. 그럴 때 쓰는 게 noexcept 지정자예요. 이 지정자는 함수가 예외를 던질 수 있는지 없는지를 명시해 줘요.

출처: cppreference - noexcept specifier

본문

구문

형태 설명
noexcept (1) noexcept(true)와 같아요.
noexcept(expression) (2) 표현식이 true로 평가되면 함수가 예외를 던지지 않는다고 선언해요. noexcept 바로 뒤의 (는 항상 이 형태의 일부예요. (초기화식을 시작할 수 없어요.)
throw() (3) noexcept(true)와 같아요. (C++17에서 폐기 예정, C++20에서 제거. C++17 이전의 의미론은 동적 예외 지정 참고)
  • expressionbool 타입으로 문맥상 변환되는 상수 표현식(contextually converted constant expression)이에요.

설명 (Explanation)

noexcept 지정은 동적 예외 지정과 마찬가지로 함수 타입의 일부가 아니에요(until C++17). 함수, 변수, 함수 타입의 비정적 데이터 멤버, 함수 포인터, 함수에 대한 참조, 멤버 함수 포인터를 선언할 때, 그리고 그런 선언에서 함수에 대한 포인터·참조가 되는 매개변수·반환 타입을 선언할 때 — 람다 선언자나 최상위 함수 선언자의 일부로만 나타날 수 있어요. typedef나 타입 별칭 선언에는 쓸 수 없어요.

void f() noexcept; // the function f() does not throw
void (*fp)() noexcept(false); // fp points to a function that may throw
void g(void pfa() noexcept);  // g takes a pointer to function that doesn't throw
// typedef int (*pf)() noexcept; // error

C++17부터는 noexcept 지정이 함수 타입의 일부가 되고, 어떤 함수 선언자에도 나타날 수 있어요. (since C++17)

C++의 모든 함수는 예외를 던지지 않는(non-throwing) 함수거나 잠재적으로 던지는(potentially-throwing) 함수예요.

잠재적으로 던지는 함수는 다음과 같아요:

  • 비어 있지 않은 동적 예외 지정으로 선언된 함수 (until C++17)
  • noexcept 지정자의 표현식이 false로 평가되는 함수
  • noexcept 지정자 없이 선언된 함수 — 다만 다음은 예외예요:
    • 소멸자 (잠재적으로 생성되는(potentially-constructed) 기반·멤버의 소멸자가 잠재적으로 던지는 경우 제외, 아래 참고)
    • 첫 선언에서 암시적으로 선언되거나 default로 정의된 기본 생성자·복사 생성자·이동 생성자 — 단, 그 생성자의 암시적 정의가 호출할 기반·멤버 생성자가 잠재적으로 던지거나, 그런 초기화의 부분식(예: 기본 인자 표현식)이 잠재적으로 던지거나, 기본 멤버 초기화(default member initializer, 기본 생성자에만)가 잠재적으로 던지는 경우 제외
    • 첫 선언에서 암시적으로 선언되거나 default로 정의된 복사 할당·이동 할당 연산자 — 단, 암시적 정의 안의 할당 연산자 호출이 잠재적으로 던지는 경우 제외
    • 첫 선언에서 default로 정의된 비교 연산자 — 단, 암시적 정의 안의 비교 연산자 호출이 잠재적으로 던지는 경우 제외 (since C++20)
    • 할당 해제 함수(deallocation function)

던지지 않는 함수는 그 외의 모두예요 — noexcept 지정자의 표현식이 true로 평가되는 함수, 소멸자, default로 정의된 특수 멤버 함수, 할당 해제 함수가 여기에 해당해요.

명시적 인스턴스화(explicit instantiation)는 noexcept 지정을 쓸 수 있지만 필수는 아니에요. 쓰는 경우 예외 지정은 다른 모든 선언과 같아야 해요. 진단은 한 번역 단위 안에서 예외 지정이 다를 때만 요구돼요.

예외 지정만 다른 함수는 오버로드할 수 없어요. (반환 타입처럼 예외 지정이 함수 타입의 일부이지만 함수 시그니처의 일부는 아니기 때문이에요.)

void f() noexcept;
void f(); // error: different exception specification
void g() noexcept(false);
void g(); // ok, both declarations for g are potentially-throwing

던지지 않는 함수에 대한 포인터(멤버 함수 포인터 포함)는 잠재적으로 던지는 함수에 대한 포인터에 할당할 수 있고 초기화에 쓸 수 있어요(until C++17), 암시적으로 변환돼요(since C++17). 반대 방향은 안 돼요.

void ft(); // potentially-throwing
void (*fn)() noexcept = ft; // error

가상 함수가 던지지 않는 경우, 오버라이더(overrider)의 모든 선언과 정의도 던지지 않아야 해요. 오버라이더가 deleted로 정의된 경우는 예외예요:

struct B
{
    virtual void f() noexcept;
    virtual void g();
    virtual void h() noexcept = delete;
};

struct D: B
{
    void f();          // ill-formed: D::f is potentially-throwing, B::f is non-throwing
    void g() noexcept; // OK
    void h() = delete; // OK
};

던지지 않는 함수는 잠재적으로 던지는 함수를 호출하는 게 허용돼요. 예외가 던져졌을 때 핸들러를 찾는 과정에서 던지지 않는 함수의 가장 바깥 블록을 만나면 std::terminate가 호출돼요:

extern void f(); // potentially-throwing

void g() noexcept
{
    f();      // valid, even if f throws
    throw 42; // valid, effectively a call to std::terminate
}

함수 템플릿 특수화의 예외 지정은 함수 선언과 함께 인스턴스화되지 않아요. 필요할 때만 (아래에서 정의하는 방식대로) 인스턴스화돼요.

암시적으로 선언된 특수 멤버 함수의 예외 지정도 필요할 때만 평가돼요. 특히 파생 클래스의 멤버 함수 암시적 선언이 기반 클래스 멤버 함수의 예외 지정을 인스턴스화하도록 요구하지 않아요.

함수 템플릿 특수화의 noexcept 지정이 필요하지만 아직 인스턴스화되지 않았다면, 종속 이름이 조회되고 표현식에 쓰인 템플릿들이 그 특수화의 선언을 위해서처럼 인스턴스화돼요.

함수의 noexcept 지정이 필요한 것으로 간주되는 문맥은 다음과 같아요:

  • 표현식에서 오버로드 결정에 의해 그 함수가 선택된 경우
  • 함수가 odr-used인 경우
  • 함수가 odr-used일 텐데 평가되지 않는 피연산자 안에 나타나는 경우
template<class T>
T f() noexcept(sizeof(T) < 4);

int main()
{
    decltype(f<void>()) *p; // f unevaluated, but noexcept-spec is needed
                            // error because instantiation of the noexcept specification
                            // calculates sizeof(void)
}
  • 다른 함수 선언과 비교하기 위해 지정이 필요한 경우 (예: 가상 함수 오버라이더, 함수 템플릿의 명시적 특수화)
  • 함수 정의 안에서
  • default로 정의된 특수 멤버 함수가 자신의 예외 지정을 결정하기 위해 확인이 필요한 경우 (이 경우는 그 default 특수 멤버 함수의 지정이 필요할 때만 일어나요)

잠재적으로 던지는 표현식의 형식적 정의 (소멸자·생성자·할당 연산자의 기본 예외 지정을 결정하는 데 쓰여요):

표현식 e는 다음 중 하나에 해당하면 잠재적으로 던지는(potentially-throwing) 표현식이에요:

  • e가 잠재적으로 던지는 함수·함수 포인터·멤버 함수 포인터에 대한 함수 호출인 경우 (단, e가 핵심 상수 표현식이면 제외, until C++17)
  • e가 잠재적으로 던지는 함수에 대한 암시적 호출을 하는 경우 (오버로드된 연산자, new 표현식 안의 할당 함수, 함수 인자의 생성자, e가 full-expression일 때의 소멸자 등)
  • ethrow 표현식인 경우
  • e가 다형적 참조 타입을 캐스팅하는 dynamic_cast인 경우
  • e가 다형적 타입에 대한 역참조 포인터에 적용된 typeid 표현식인 경우
  • e가 잠재적으로 던지는 직접 부분식(immediate subexpression)을 갖는 경우
struct A
{
    A(int = (A(5), 0)) noexcept;
    A(const A&) noexcept;
    A(A&&) noexcept;
    ~A();
};

struct B
{
    B() throw();
    B(const B&) = default; // implicit exception specification is noexcept(true)
    B(B&&, int = (throw Y(), 0)) noexcept;
    ~B() noexcept(false);
};

int n = 7;
struct D : public A, public B
{
    int * p = new int[n];
    // D::D() potentially-throwing because of the new operator
    // D::D(const D&) non-throwing
    // D::D(D&&) potentially-throwing: the default argument for B’s constructor may throw
    // D::~D() potentially-throwing

    // note; if A::~A() were virtual, this program would be ill-formed because an overrider
    // of a non-throwing virtual cannot be potentially-throwing
};

참고 (Notes)

상수 표현식은 noexcept 연산자와 함께 쓰여, 어떤 타입에 대해서는 noexcept로 선언하고 다른 타입에 대해서는 그렇지 않은 함수 템플릿을 정의하는 데 유용해요.

한 가지 짚고 넘어갈 점이 있어요. 함수의 noexcept 지정은 컴파일 타임 검사가 아니에요. 프로그래머가 컴파일러에게 "이 함수는 예외를 던지지 않을 거야"라고 알려주는 수단일 뿐이죠. 컴파일러는 이 정보를 써서 던지지 않는 함수에 몇몇 최적화를 적용하고, noexcept 연산자도 쓸 수 있게 해요. noexcept 연산자는 특정 표현식이 예외를 던질 것으로 선언되어 있는지를 컴파일 타임에 검사해 주죠. 예를 들어 std::vector 같은 컨테이너는 요소의 이동 생성자가 noexcept면 요소를 이동하고, 그렇지 않으면 복사해요. (복사 생성자가 접근 가능하지 않고 잠재적으로 던지는 이동 생성자가 있는 경우는 강한 예외 보장이 무시되고 복사하는 대신 이동해요.)

폐기된 것 (Deprecates)

noexcept는 C++11에서 폐기 예정이 된 throw()의 개선 버전이에요. C++17 이전의 throw()와 달리 noexceptstd::unexpected를 호출하지 않고, 스택을 풀 수도 있고 아닐 수도 있으며, std::terminate를 호출해요. 덕분에 컴파일러는 throw()가 가진 런타임 오버헤드 없이 noexcept를 구현할 수 있어요. C++17부터 throw()noexcept(true)와 완전히 동등한 것으로 재정의되었어요.

기능 테스트 매크로 표준 기능
__cpp_noexcept_function_type 201510L (C++17) 예외 지정을 타입 시스템의 일부로 만듦

키워드

noexcept, throw(C++17 이후, C++20까지)

예제

noexcept 지정이 어떻게 함수 템플릿에서 쓰이는지 볼게요.

// whether foo is declared noexcept depends on if the expression
// T() will throw any exceptions
template<class T>
void foo() noexcept(noexcept(T())) {}

void bar() noexcept(true) {}
void baz() noexcept { throw 42; } // noexcept is the same as noexcept(true)

int main()
{
    foo<int>(); // noexcept(noexcept(int())) => noexcept(true), so this is fine

    bar(); // fine
    baz(); // compiles, but at runtime this calls std::terminate
}

baz()noexcept로 선언됐는데 안에서 throw 42;를 해요. 컴파일은 되지만, 예외가 던져지면 noexcept 함수에서는 핸들러를 찾을 수 없으니 런타임에 std::terminate가 호출돼요.

결함 보고 (Defect reports)

다음은 이전에 발행된 C++ 표준에 소급 적용된, 동작을 바꾸는 결함 보고예요.

DR 적용 대상 발행 당시 동작 올바른 동작
CWG 1330 C++11 예외 지정이 조기에 인스턴스화될 수 있었음 필요할 때만 인스턴스화됨
CWG 1740 C++11 noexcept 뒤의 (가 초기화식을 시작할 수 있었음 noexcept 지정의 일부일 수만 있음
CWG 2039 C++11 변환 전의 표현식만 상수이면 됐음 변환도 상수 표현식에서 유효해야 함

더 알아보기 (Learn more)

  • noexcept 연산자 — 표현식이 예외를 던지는지 판단하는 연산자예요. 지정자와 짝을 이뤄요.
  • 동적 예외 지정 — 함수가 어떤 예외를 던지는지 지정하는 옛 방식이에요. C++11에서 폐기 예정이 되었어요.
  • throw 표현식 — 오류를 알리고 제어를 오류 처리기로 넘기는 표현식이에요.
  • std::move_if_noexcept — 이동 생성자가 예외를 던지지 않으면 xvalue로 변환해 주는 함수 템플릿이에요.