Ch19.6: Crossing the Boundary
1. Legacy to Herbceptions: Automatic
You will call libraries that report failures by throwing, and you cannot change them.
Nothing needs to be written for that direction: a legacy exception that reaches a
throws region — a call inside a catch throws handler, or a
call from a function annotated throws — is converted into a
::std::error for you, with no source change and no wrapper:
#include <fast_io.h>
#include <stdexcept>
/* a library we do not own: noexcept(false) is the legacy spelling of
"this function may throw", and it is the only contract it can state */
void legacy_parser(int value) noexcept(false)
{
if (value < 0)
{
throw ::std::runtime_error("negative input");
}
}
/* our code: failures travel on the throws channel */
int parse(int value) throws
{
legacy_parser(value);
return value;
}
int main()
{
try
{
::fast_io::io::print("parse(7) = ", parse(7), "\n");
int const bad{-1};
::fast_io::io::print("parse(-1) = ", parse(bad), "\n");
}
catch throws (::std::error e)
{
::fast_io::io::print("caught ", e, "\n");
}
}
parse(7) = 7
caught [itanium_exception](St13runtime_error)negative input
parse is a throws function and never mentions exceptions. The
legacy library throws one anyway, and it arrives at the caller as a two-word value in the
platform's exception domain — itanium_exception here, i.e. "this is a
boxed C++ exception object". The message text is the runtime's rendering of the thrown
object: (St13runtime_error) is the mangled type name and
negative input is what().
Note what this conversion is not: it is not a catch clause you have to write, and it does not cost you a legacy try block. The unwind still happens inside the legacy library; what crosses into your code is only the result. That is the whole point of the design — the boundary is paid once, by whoever touches the legacy library, and is invisible to everyone above it.
2. Rethrowing a Herbception
Inside a handler, a bare throw throws; re-raises the same error with no copy
and no new information, exactly like the legacy throw;. This is how you add
context, or decide that certain codes are not yours to swallow:
#include <fast_io.h>
int read_value(int value) throws
{
try
{
if (value < 0)
{
throw throws ::std::errc::invalid_argument;
}
return value;
}
catch throws (::std::error e)
{
if (e.equivalent(::std::errc::invalid_argument))
{
throw throws;
}
return -1;
}
}
int main()
{
try
{
::fast_io::io::print("read_value(5) = ", read_value(5), "\n");
::fast_io::io::print("read_value(-1) = ", read_value(-1), "\n");
}
catch throws (::std::error e)
{
::fast_io::io::print("caught ", e, "\n");
}
}
read_value(5) = 5
caught [posix](22)Invalid argument
The transcript is worth reading carefully. The first line printed, the second threw, and
the text read_value(-1) = never appeared — because print
evaluates its arguments before it writes anything, so a failure while formatting means no
partial output. That is a property of a single print call, and one more reason
to write one call per line instead of assembling a line from many writes.
3. Herbceptions to Legacy: You Must Ask
The reverse direction is not automatic, and it must not be. Converting a herbception into a C++ exception means paying for the unwinder, the heap allocation and the RTTI that Ch19.8 measured — a cost that would silently appear in code that never asked for it. So the language makes you write it:
#include <fast_io.h>
#include <stdexcept>
int main()
{
try
{
try
{
throw throws ::std::errc::value_too_large;
}
catch throws (::std::error e)
{
/* a handler cannot call a throwing function: no second channel */
e.throw_legacy_exception();
}
}
catch (::std::exception const &ex)
{
::fast_io::io::print("legacy side ", ::fast_io::mnp::os_c_str(ex.what()), "\n");
}
}
legacy side [posix](75)Value too large for defined data type
e.throw_legacy_exception() is [[noreturn]]: it either re-throws
the error as the platform's native exception type (when the domain implements
do_throw_legacy_exception) or boxes it into
::std::cxx_std_error_legacy_exception, which derives from
::std::exception and caches its message at construction. That is the box you
caught: what() returned [posix](75)Value too large for defined data
type — the domain name, the code and the message, produced by the same
do_query_information hook that prints errors everywhere else.
Two things are lost on the way and you should know both. A code that owns memory
(a domain with a non-null do_cleanup) cannot be boxed by copying — two
copies would free the same thing — so it is first demoted through
do_to_errc into the POSIX domain, which owns nothing. And the whole point of a
domain — a code space with a name — collapses into a message string and a
what(). That is what the legacy mechanism can carry, so that is what survives.
There is also a faster sibling, throw_legacy_exception_no_what(), which skips
the message query and therefore performs no allocation at throw time. Use it when the
legacy code only catches to clean up and never reads the text.
And there is a rule about the handler you are in: keep it simple. A
catch throws (::std::error e) handler has no second channel, so it should
convert and leave, not do work that can fail.