Ch6.16: Every Function Is noexcept or throws

中文(简体)

IMPORTANT — read this together with Ch3.16. In this tutorial a function is never left without an error contract. Every function you write — free function, member function, constructor, lambda, virtual override — is declared either noexcept (it cannot fail) or throws (it can report an error to its caller). The single exception in the whole language is int main(), which has no caller to report to.

Overview

In Ch3.16 you learned to report an error with throw throws and to handle it with catch throws. Both of those statements are only legal inside a function that has been marked, and this chapter is about that mark: the exception specification, which in herbceptions is a two-valued contract.


int  add(int a, int b) noexcept;   // cannot fail
int  div(int a, int b) throws;     // can fail: caller must handle or propagate

There is no third option, and there is no "I did not think about it" option. The compiler refuses to let a function stay unspecified in a way that leaks failures: a call to a throws function is only allowed if the caller either handles it or is itself throws.

1. Why a Function Must Say

Consider the caller. It has two questions about every call:

A function declaration answers both, at compile time. That is the entire design: the error contract is part of the type, so the compiler can verify the whole call graph without any documentation, and without any runtime cost.

Note: under legacy C++ exceptions, functions are allowed to omit the spec and "throw anything". That freedom is exactly what makes legacy exceptions unanalysable: any function below you in the call graph can fail, so every function must be written as if it can. Herbceptions remove the freedom, and with it the problem.

2. noexcept: I Cannot Fail

A noexcept function promises that it has nothing to report. The promise is checked by the compiler:


#include <fast_io.h>

using namespace ::fast_io::io;

int square(int x) noexcept
{
    return x * x;
}

int bad(int x) noexcept
{
    throw throws ::std::errc::invalid_argument;   // ERROR: noexcept cannot report
}

int main()
{
    println(square(7));
}

The compiler says:


error: 'throw throws' is only allowed inside a function declared 'throws' or 'return_failure{...}'

The same happens for an unhandled call:


::std::uint_least64_t parse_port(int raw) throws;   // can fail

void forward_port(int raw) noexcept
{
    println(parse_port(raw));   // ERROR: forward_port promised it cannot fail
}

error: call to 'throws' function in a non-'throws' function must be handled by an
enclosing 'try { } catch throws' block, or mark the calling function as 'throws' so
herbceptions can propagate

noexcept functions are everywhere you need a guaranteed-infallible piece of code: comparison operators, size queries, bit tricks, validation helpers, the body of a destructor, and any callback whose API cannot deliver an error.

3. throws: I Can Fail, and You Will Know

A throws function reports by throw throws, and the error is handed to the caller as a ::std::error (see Ch3.16 for every member function of that value). Nothing about the call site changes: no out parameter, no error-code return, no errno to check.


#include <fast_io.h>

using namespace ::fast_io::io;

::std::uint_least64_t parse_port(int raw) throws
{
    if (raw <= 0 || raw > 65535)
    {
        throw throws ::std::errc::value_too_large;
    }
    return static_cast<::std::uint_least64_t>(raw);
}

void print_port(int raw) throws          // propagates: no handler here
{
    println("port: ", parse_port(raw));
}

int main()
{
    try
    {
        print_port(8080);
        print_port(99999);              // fails here
    }
    catch throws (::std::error e)
    {
        perrln(e);
        return 1;
    }
}

Output:


port: 8080
[posix](75)Value too large for defined data type

Notice what did not have to happen: parse_port did not return a sentinel, print_port did not test one, and main did not inspect an error code after each call. The failure travelled straight from the place that detected it to the place that decided to report it.

A throws function is the right default for anything that touches the outside world: opening files, allocating, reading, writing, parsing, connecting. That is why fast_io marks them all that way.

4. Handling Inside a noexcept Function

A noexcept function may still call fallible code — it just has to absorb the failure itself. There are two honest ways:


#include <fast_io.h>

using namespace ::fast_io::io;

int read_count(int n) throws
{
    if (n < 0)
    {
        throw throws ::std::errc::invalid_argument;
    }
    return n * 2;
}

// 1. recover: turn the failure into a value the caller understands
int read_count_or_zero(int n) noexcept
{
    try
    {
        return read_count(n);
    }
    catch throws (::std::error)
    {
        return 0;
    }
}

// 2. ignore: the failure genuinely cannot be acted on here
void try_log(int n) noexcept
{
    try
    {
        println("count: ", read_count(n));
    }
    catch throws (::std::error)
    {
    }
}

int main()
{
    println(read_count_or_zero(-1), " ", read_count_or_zero(21));
    try_log(-5);                        // logs nothing, does not crash
}

The second form — the empty named-less handler catch throws (::std::error) { } — is the only sanctioned way to swallow a herbception, and it is only correct where reporting is impossible or pointless. Never swallow to "keep going" in a function that could report.

Do not swallow what you cannot see. If the handler body prints the error (perrln(e)), the handler itself can fail — printing is I/O — so the enclosing function cannot be noexcept. Reporting handlers belong in throws functions (or in main), and that is why the example above uses the empty form.

5. Constructors and Destructors

Constructors follow the same rule: if construction can fail, the constructor is throws. A type that owns an OS resource is the classic case — opening a file really can fail, so the object cannot exist in a half-open state:


struct resource
{
    int handle;

    resource(int raw) throws                      // construction may fail
    {
        if (raw < 0)
        {
            throw throws ::std::errc::bad_file_descriptor;
        }
        handle = raw;
    }

    ~resource()                                   // implicitly noexcept
    {
    }
};

Destructors are implicitly noexcept — make it explicit if you like (~resource() noexcept), but you cannot opt out. This is deliberate: a destructor runs while the program is already leaving a scope, and there is nobody to report to. Consequently a destructor may not call an unhandled throws function:


struct writer
{
    ~writer()
    {
        println("bye");      // ERROR: destructors cannot report failures
    }
};

When a cleanup step can fail and the failure matters, do it before the object dies, in a throws function: call flush() / close() explicitly and check it there. Letting the destructor do best-effort cleanup is fine; relying on the destructor for error detection is not.

6. The Specification Is Part of the Type

Because the compiler must be able to reason about it, the specification is part of the function type, exactly like the parameter types. Four consequences you will meet in practice:

You cannot overload on it


int f(int a) noexcept;
int f(int a) throws;      // ERROR

error: exception specification in declaration does not match previous declaration

Function pointers carry it


int pure(int a) noexcept { return a + 1; }
int risky(int a) throws  { return a + 1; }

int (*p1)(int) noexcept = pure;      // ok
int (*p2)(int) throws   = risky;     // ok
int (*p3)(int) throws   = pure;      // ERROR: different types

error: cannot initialize a variable of type 'int (*)(int) throws' with an lvalue of
type 'int (int) noexcept': different exception specifications

There is no implicit conversion in either direction: a caller that expects a throws function must not silently receive a pointer whose contract it cannot rely on, and vice versa.

Virtual overrides must match


struct base
{
    virtual void f() noexcept {}
};

struct derived : base
{
    void f() throws override {}      // ERROR
};

error: overriding function has a different herbceptions ('throws'/'return_failure{...}')
specifier than the base version, which changes the calling convention

Lambdas and member functions take the spec too


struct point
{
    int x, y;

    int sum() const noexcept { return x + y; }
};

int main()
{
    auto pure_lambda  = []() noexcept { return 1; };
    auto risky_lambda = []() throws   { return 2; };
    point p{1, 2};
    ::fast_io::println(pure_lambda(), risky_lambda(), p.sum(), "\n");
}

7. Conditional Specifications (for Templates)

Inside a template you cannot always say the answer up front: whether your function can fail depends on the operations on T. Both forms accept a bool constant expression, and noexcept(expression) answers "would that expression's call be unable to fail?":


int pure(int a) noexcept { return a + 1; }
int risky(int a) throws  { return a; }

// 'noexcept(...)' is itself a compile-time bool
int cond1(int a) noexcept(noexcept(pure(a)))       { return pure(a); }
int cond2(int a) throws(!noexcept(risky(a)))       { return risky(a); }

noexcept(expression) is the tool you will use constantly when reading fast_io's own headers: every operation there is annotated with a conditional specification derived from the operations it performs, so that println is throws for a real file and stays noexcept for a sink that cannot fail.


#include <fast_io.h>

using namespace ::fast_io::io;

// ask the library, do not guess
static_assert(!noexcept(println(1)));                                // I/O can fail
static_assert(!noexcept(scan(::fast_io::c_stdin(),
                             ::std::declval<int&>())));             // input can fail
static_assert(!noexcept(::fast_io::to<::std::uint_least64_t>>("12"))); // parsing can fail
static_assert(noexcept(::fast_io::cstr_len("abc")));                 // pure computation

8. main Is the One Exception

main has no caller, so there is nobody to report to. The language therefore forbids a specification on it:


int main() throws { }    // ERROR

error: 'main' cannot be declared with a herbceptions ('throws'/'return_failure{...}')
exception specification; 'main' has no caller able to consume an error result, and
uncaught errors in 'main' trap instead

So main is where the error chain ends: it calls throws functions inside a try block, prints what went wrong, and returns an exit code. That is the shape you already saw in Ch2.14:


#include <fast_io.h>

int main()
{
    try
    {
        ::fast_io::println("work\n");
    }
    catch throws (::std::error e)
    {
        ::fast_io::perrln(e);
        return 1;
    }
    return 0;
}
Corollary: main may also leave a call unhandled in practice (the compiler treats main as able to propagate), but then a failure traps with no message. Always catch in main; a trapped process with no diagnostic is a bug report you have to write yourself.

9. The Rule in One Table

Situation What to declare
pure computation: arithmetic, comparisons, accessors, bit tricks noexcept
anything that can touch a resource: file, socket, allocation, parse, I/O throws
a helper that reports the failure further up throws (use throw throws; inside handlers)
a function that must not fail but calls fallible code noexcept + a local handler (recover, or the empty form)
destructors implicitly noexcept; do explicit flush()/close() in a throws function when the failure matters
constructors that acquire a resource throws
generic code noexcept(...)/throws(...) conditional spec
int main() no spec; catch and report

Key Takeaways