Ch19.4: <stdexcept>

中文(简体)

1. The Header Everyone Includes

<stdexcept> is the class library that came with legacy exceptions, and it is the one header that every C++ codebase includes without thinking. Its entire content is a two-branch tree below ::std::exception:


std::exception                     what()  ->  char const *
├── std::logic_error                 "violated a precondition the caller could have checked"
│   ├── std::invalid_argument
│   ├── std::domain_error
│   ├── std::length_error
│   └── std::out_of_range
└── std::runtime_error               "only detectable while running"
    ├── std::range_error
    ├── std::overflow_error
    ├── std::underflow_error
    └── std::system_error            (<system_error>, Ch19.5)

Four more classes live in other headers and are thrown by the runtime itself: ::std::bad_alloc and ::std::bad_array_new_length in <new>, ::std::bad_cast and ::std::bad_typeid in <typeinfo>. Every one of them stores a message string and returns it from a virtual what().

2. What the Split Means, and Why It Carries Nothing

The standard's intent is: logic_error is for a bug in the caller, a precondition that could have been checked before the call; runtime_error is for something only the running program can discover. That sounds like information, and it is the reason tutorials spend a page on it.

It carries nothing. No compiler checks it, no catch site can act on it, and the distinction is invisible at the call site because the function's signature does not mention the type. A caller in a catch (::std::exception const &) has no way to tell whether it violated a precondition or hit a full disk, and after the fact it does not matter: the recovery is the same "give up on this operation" path. What actually carries information is the code, which is what a herbception puts in its two words (Ch3.16).

Class The standard's intent What a program can do with it
invalid_argument A parameter was wrong Nothing; it arrives after the call
out_of_range An index or key was outside its domain Nothing; the check it describes is one you should have made yourself
length_error A container cannot hold that many elements Print a message nobody reads
overflow_error / underflow_error Arithmetic went out of range Nothing
bad_alloc Memory ran out Almost never anything correct — Ch19.2 argues it should terminate

3. Constructing One, and What It Stores

The constructors are (char const *) and (::std::string const &). The message is stored inside the object, which means the constructor of a runtime_error with a long message allocates — on the failure path, before the exception has even been thrown.

what() returns char const *, and in this book that is not printable as text (Ch5.8): you must say what the pointer means, so every example here writes ::fast_io::mnp::os_c_str(e.what()). A C string is a convention, not a type.

To add anything of your own to a legacy exception you must derive from one of these classes and pass a message up to it. There is no other way: the base class owns the only channel out. Here is a parser error with a line and a column, caught as its base, next to the standard library's own length_error from a container:


#include <fast_io.h>
#include <stdexcept>
#include <string>
#include <vector>

/* the only way to add data to a legacy exception: derive and pass a message up */
struct parse_error : ::std::runtime_error
{
	::std::size_t line;
	::std::size_t column;
	parse_error(::std::size_t l, ::std::size_t c)
		: ::std::runtime_error("invalid token"), line(l), column(c)
	{}
};

int main()
{
	try
	{
		throw parse_error(12, 34);
	}
	catch (::std::exception const &e)
	{
		::fast_io::io::print("as exception ", ::fast_io::mnp::os_c_str(e.what()), "\n");
	}
	try
	{
		::std::vector<int> v;
		v.resize(static_cast<::std::size_t>(1) << 63);
	}
	catch (::std::exception const &e)
	{
		::fast_io::io::print("as length     ", ::fast_io::mnp::os_c_str(e.what()), "\n");
	}
}


as exception invalid token
as length     vector::_M_default_append

Two things about that transcript. First, the parse_error's own data — line and column — is gone at the catch site: the handler was written for ::std::exception, so the only thing it can reach is what(). The extra data exists, was paid for, and cannot be recovered without RTTI and a dynamic_cast (Ch18.5).

Second, the standard library's message is vector::_M_default_append — the name of a private member function, not a sentence about the user's problem. That is what what() is in practice: an implementation detail string, in English, that no program can parse and no user can act on.

4. The Bug the Header Invites: Catching by Value

catch (T e) is legal for every T, and for a polymorphic T it copies only the T part of the exception object. The rest of the object is sliced off, including the vtable, so the what() you call through the base is the base's:


#include <fast_io.h>
#include <stdexcept>

int main()
{
	try
	{
		throw ::std::out_of_range("index 7 is past the end");
	}
	catch (::std::exception e)
	{
		::fast_io::io::print("what ", ::fast_io::mnp::os_c_str(e.what()), "\n");
	}
}


slice.cc: In function 'int main()':
slice.cc:10:33: warning: catching polymorphic type 'class std::exception' by value [-Wcatch-value=]
   10 |         catch (::std::exception e)
      |                                 ^
what std::exception

The program threw out_of_range("index 7 is past the end") and printed std::exception. The message is not lost, it is sliced away: the copy the handler owns is a ::std::exception subobject, and its what() says so. The compiler will tell you about it if you ask for warnings (-Wcatch-value), and every legacy style guide has a line about catching by const& because of this.

5. What Replaces It

Nothing in the herbceptions world corresponds to <stdexcept>, and nothing needs to. An error is a code in a domain (Ch19.1): the code says what happened, the domain says what the code means, and to_errc() gives every caller a common tongue. There is no message string to allocate, no base class to derive from, no virtual what() to slice, and no handler ordering to get wrong.

When a legacy library throws one of these at you anyway, it arrives as a ::std::error with a domain and a message (Ch19.6), which is the whole reason the interop layer exists.