Proposed SG14 status_code for the C++ standard
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.

Readme.md

Reference implementation for proposed SG14 status_code (<system_error2>) in C++ 11

(C) 2018 Niall Douglas http://www.nedproductions.biz/ Please send feedback to the SG14 study group mailing list at https://groups.google.com/a/isocpp.org/d/forum/sg14.

Docs: https://ned14.github.io/status-code/ (reference API docs are at bottom of page) Linux: Build Status Windows: Build status

Solves the problems for low latency/large code base users with <system_error> as listed by WG21 P0824. This proposed <system_error2> library is EXPERIMENTAL and is subject to change as the committee evolves the design. To fetch a drop-in standalone single file implementation:

wget https://github.com/ned14/status-code/raw/master/single-header/system_error2.hpp

Features:

  • Portable to any C++ 11 compiler. These are known to work:
    • >= GCC 5 (due to requiring libstdc++ 5 for sufficient C++ 11 type traits)
    • >= clang 3.3 with a new enough libstdc++ (previous clangs don't implement inheriting constructors)
    • >= Visual Studio 2015 (previous MSVC's don't implement inheriting constructors)
  • Comes with built in POSIX, Win32, NT kernel, Microsoft COM and std::error_code status code domains.
  • Implements std::error as proposed by P0709 Zero-overhead deterministic exceptions.
  • Aims to cause zero code generated by the compiler most of the time.
  • Never calls malloc().
  • Header-only library friendly.
  • Type safe yet with type erasure in public interfaces so it can scale across huge codebases.
  • Minimum compile time load, making it suitable for use in the global headers of multi-million line codebases.

Example of use:

POSIX Windows
using native_handle_type = int;
native_handle_type open_file(const char *path,
  system_error2::system_code &sc) noexcept
{
  sc.clear();  // clears to empty
  native_handle_type h = ::open(path, O_RDONLY);
  if(-1 == h)
  {
    // posix_code type erases into system_code
    sc = system_error2::posix_code(errno);
  }
  return h;
}
using native_handle_type = HANDLE;
native_handle_type open_file(const wchar_t *path,
  system_error2::system_code &sc) noexcept
{
  sc.clear();  // clears to empty
  native_handle_type h = CreateFile(path, GENERIC_READ,
    FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
    nullptr,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    nullptr
  );
  if(INVALID_HANDLE_VALUE == h)
  {
    // win32_code type erases into system_code
    sc = system_error2::win32_code(GetLastError());
  }
  return h;
}
Portable code
system_error2::system_code sc;  // default constructs to empty
native_handle_type h = open_file(path, sc);
// Is the code a failure?
if(sc.failure())
{
  // Do semantic comparison to test if this was a file not found failure
  // This will match any system-specific error codes meaning a file not found
  if(sc != system_error2::errc::no_such_file_or_directory)
  {
    std::cerr << "FATAL: " << sc.message().c_str() << std::endl;
    std::terminate();
  }
}

Problems with <system_error> solved:

  1. Does not cause #include <string>, and thus including the entire STL allocator and algorithm machinery, thus preventing use in freestanding C++ as well as substantially impacting compile times which can be a showstopper for very large C++ projects. Only includes the following headers:

    • <atomic> to reference count localised strings retrieved from the OS.
    • <cassert> to trap when misuse occurs.
    • <cerrno> for the generic POSIX error codes (errno) which is required to define errc.
    • <cstddef> for the definition of size_t and other types.
    • <cstring> for the system call to fetch a localised string and C string functions.
    • <exception> for the basic std::exception type so we can optionally throw STL exceptions.
    • <initializer_list> so we can permit in-place construction.
    • <new> so we can perform placement new.
    • <type_traits> as we need to do some very limited metaprogramming.
    • <utility> if on C++ 17 or later for std::in_place.

    All of the above headers are on the "fast parse" list at https://github.com/ned14/stl-header-heft.

    These may look like a lot, but in fact just including <atomic> on libstdc++ actually brings in most of the others in any case, and a total of 200Kb (8,000 lines) of text is including by system_error2.hpp on libstdc++ 7. Compiling a file including status_code.hpp takes less than 150 ms with clang 3.3 as according to the -ftime-report diagnostic (a completely empty file takes 5 ms).

  2. Unlike std::error_code which was designed before constexpr, this proposed implementation has all-constexpr construction and destruction with as many operations as possible being trivial or literal, with only those exact minimum operations which require runtime code generation being non-trivial (note: requires C++ 14 for a complete implementation of this).

  3. This in turn means that we solve a long standing problem with std::error_category in that it is not possible to define a safe custom C++ 11 error category in a header only library where semantic comparisons would randomly break depending on the direction of wind blowing when the linker ran. This proposed design is 100% safe to use in header only libraries.

  4. std::error_code's boolean conversion operator i.e. if(ec) ... has become unfortunately ambiguous in real world C++ out there. Its correct meaning is "if ec has a non-zero value". Unfortunately, much code out in the wild uses it as if "if ec is errored". This is incorrect, though safe most of the time where ec's category is well known i.e. non-zero values are always an error. For unknown categories supplied by third party code however, it is dangerous and leads to unpleasant, hard-to-debug, surprise.

    The status_code proposed here suffers from no such ambiguity. It can be one of exactly three meanings: (i) success (ii) failure (iii) empty (uninitialised). There is no boolean conversion operator, so users must write out exactly what they mean e.g. if(sc.success()) ..., if(sc.failure()) ..., if(sc.empty()) ....

  5. Relatedly, status_code can now represent successful (informational) codes as well as failure codes. Unlike std::error_code where zero is given special meaning, we impose no requirements at all on the choice of coding. This permits safe usage of more complex C status coding such as the NT kernel's NTSTATUS, which is a LONG whereby bits 31 and 30 determine which of four categories the status is (success, informational, warning, error), or the very commone case where negative numbers mean failure and positive numbers mean success-with-information.

  6. The relationship between std::error_code and std::error_condition is confusing to many users reading code based on <system_error>, specifically when is a comparison between codes semantic or literal? status_code makes all comparisons semantic, always. If you want a literal comparison, you can do one by hand by comparing domains and values directly.

  7. std::error_code enforced its value to always be an int. This is problematic for coding systems which might use a long and implement coding namespaces within the extended number of bits, or for end users wishing to combine a code with a void * in order to transmit payload or additional context. As a result, status_code is templated to its domain, and the domain sets its type. A type erased edition of status_code<D> is available as status_code<void>, this is for obvious reasons non-copyable, non-movable and non-destructible.

    A more useful type erased edition is status_code<erased<T>> which is available if D::value_type is trivially copyable, T is an integral type, and sizeof(T) >= sizeof(D::value_type). This lets you use status_code<erased<T>> in all your public interfaces without restrictions. As a pointer to the original category is retained, and trivially copyable types may be legally copied by memcpy(), type erased status codes work exactly as normal, except that publicly it does not advertise its type.

  8. std::system_category assumes that there is only one "system" error coding, something mostly true on POSIX, but not elsewhere. This library defines system_code to a type erased status code sufficiently large enough to carry any of the system error codings on the current platform. This allows code to construct the precise error code for the system failure in question, and return it type erased from the function. Depending on the system call which failed, a function may therefore return any one of many system code domains.

  9. Too much <system_error> code written for POSIX uses std::generic_category when they really meant std::system_category because the two are interchangeable on POSIX. Further confusion stems from std::error_condition also sharing the same coding and type. This causes portability problems. This library's generic_code has a value type of errc which is a strong enum. This prevents implicit confusion with posix_code, whose value type is an int same as errno returns. There is no distinction between codes and conditions in this library, rather we treat generic_code as something special, because it represents errc. The cleanup of these ambiguities in <system_error> should result in users writing clearer code with fewer unintended portability problems.