P3256R0
Formatting what matters

Draft Proposal,

Author:
Audience:
SG16, LEWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21

1. Introduction

This paper proposes making std::exception and publicly derived exception types formattable with std::format and printable with std::print. It also specifies the encoding of library-generated parts of standard exception messages, resolving [LWG4087].

2. Motivation

Exception messages are commonly written to logs or diagnostic output. Today this requires explicitly calling what():

try {
  process_request();
} catch (const std::exception& e) {
  std::println(stderr, "Request failed: {}", e.what());
}

Unfortunately, this does not work reliably. Consider the following example from [LWG4087]:

std::uintmax_t size = 0;
try {
  size = std::filesystem::file_size(L"Шчучыншчына");
} catch (const std::exception& e) {
  std::println("Памылка: {}", e.what());
}

This gives the following output on Windows even when the literal encoding is UTF-8 and the regional settings are set to Belarusian (the language of the text):

Памылка: file_size: The system cannot find the file specified.: "�����������"

Microsoft STL implements system_category().message() using FormatMessageA and first requests an en-US message using language identifier 0x0409. Since the English message is ASCII, only the filesystem path exhibits mojibake.

Trying to do the same with iostreams

std::cout << "Памылка: " << e.what() << std::endl;
gives even worse result:
╨Я╨░╨╝╤Л╨╗╨║╨░: file_size: The system cannot find the file specified.: "╪ўєў√э°ў√эр"

The situation is somewhat better on macOS and most Linux systems where UTF-8 has become de-facto standard.

The current proposal makes std::exception directly formattable and specifies a consistent encoding for messages produced by the standard library implementation itself, making both

std::println("Памылка: {}", e);
and
std::println("Памылка: {}", e.what());
work and produce the correct output such as
Памылка: file_size: The system cannot find the file specified.: Шчучыншчына
(The exact message format depends on the implementation.)

3. Proposal

The current paper proposes a formatter partial specialization for std::exception and types publicly derived from it. The default format is the string returned by the virtual what() member function. The specialization uses the standard string format specification, providing the usual fill, alignment, width, precision, and debug formatting. It also resolves [LWG4087] by specifying the encoding of library-generated exception messages.

For example:

std::runtime_error e("connection refused");
auto s1 = std::format("{}", e);       // "connection refused"
auto s2 = std::format("{:>20}", e);   // "  connection refused"
auto s3 = std::format("{:?}", e);     // "\"connection refused\""

The formatter is not opted into the nonlocking formatter optimization because what() is virtual and can execute program-defined code.

3.1. Encoding

The runtime encoding of what() is currently unspecified, making portable interpretation and display impossible in general. The encoding problem cannot be solved in the formatter: by the time it calls what(), differently encoded components may already have been combined into a single NTBS with no information about their source encodings. The message must instead be normalized when it is constructed.

For the existing const char* interface, the ordinary literal encoding is the only choice that makes library-generated messages directly composable with ordinary narrow strings on every implementation and fixes both direct and formatted output. This is consistent with the design of std::format and std::print, whose narrow-character interfaces use the ordinary literal encoding. It also matches how exception messages are formed: most are either constructed from string literals or contain literal components.

Unlike the C locale encoding, the ordinary literal encoding does not change in response to setlocale while an exception object is alive. This prevents bugs where a message is constructed under one C locale and interpreted under another.

The choice is also implementable with existing facilities. [P2319] fixed filesystem path presentation problems by adding filesystem::path::display_string(), which produces a string suitable for formatting and printing in the ordinary literal encoding. No corresponding API converts a path to the C locale encoding.

A known, stable source encoding also enables future formatting support for additional code unit types. Program-supplied what_arg bytes are preserved: when they use the ordinary literal encoding, the complete message does too; otherwise only the library-generated parts have that guarantee. Program-defined exception types remain responsible for their own encoding but now have a consistent standard-library model to follow. The formatter is currently provided only for char because what() returns a narrow string.

4. Wording

Modify paragraph 2 of [exception] as indicated:

The what() member function of each such T satisfies the constraints specified for exception::what() (see below).

For a call to what() on an object whose dynamic type is a standard library class that is exception or is derived from exception, every character sequence incorporated into the returned NTBS, other than an NTBS supplied by the program and copied without modification, is in the ordinary literal encoding. If every such copied NTBS is in the ordinary literal encoding, the returned NTBS is in the ordinary literal encoding.

Modify exception::what() in [exception] as indicated:

constexpr virtual const char* what() const noexcept;

Returns: An implementation-defined NTBS, which during constant evaluation is encoded with the ordinary literal encoding.

Remarks: The message may be a null-terminated multibyte string, suitable for conversion and display as a wstring. The return value remains valid until the exception object from which it is obtained is destroyed or a non-const member function of the exception object is called.

Modify system_error::what() in [syserr.syserr.members] as indicated:

const char* what() const noexcept override;

Returns: An NTBS incorporating the arguments supplied in the constructor.

For a constructor with a what_arg parameter, the NTBS designated by what_arg.c_str() or what_arg, as applicable, is incorporated without changing its element values. If an error-category message is incorporated, then during construction let m be code().message() and let E be the encoding of the C locale in effect when that call returns. m is transcoded from E to the ordinary literal encoding during construction and incorporated into the returned NTBS. The handling of characters not representable in the ordinary literal encoding is implementation-defined.

Modify filesystem_error::what() in [fs.filesystem.error.members] as indicated:

const char* what() const noexcept override;

Returns: An NTBS that incorporates the what_arg argument supplied to the constructor NTBS designated by what_arg.c_str() without changing its element values . The exact format is unspecified. Implementations should include the system_error::what() string and the pathnames of path1 and path2 in the native format in the returned string in the returned string. For each included pathname corresponding to a path p, p.display_string() is evaluated during construction and its result is incorporated into the returned string .

The feature-test macro below detects the formatter addition. The encoding changes are intended to be applied as a defect report and do not depend on the macro.

Add an entry for __cpp_lib_format_exception to section "Header <version> synopsis" [version.syn], in a place that respects the table’s current alphabetic order:

#define __cpp_lib_format_exception 20XXXXL // also in <format>

Modify "Header <format> synopsis" [format.syn] as indicated:

// [format.formatter], formatter
template<class T, class charT = char> struct formatter;

// [format.exception], exception formatter
template<class T>
  requires derived_from<T, exception>
struct formatter<T, char>;

// [format.formatter.locking], formatter locking
template<class T>
  constexpr bool enable_nonlocking_formatter_optimization = false;

template<class T>
  requires derived_from<T, exception>
constexpr bool enable_nonlocking_formatter_optimization<T> = false;

Add a new section "Exception formatter" [format.exception] under [format.formatter]:

template<class T>
  requires derived_from<T, exception>
struct formatter<T, char> : formatter<const char*, char> {
  template<class FormatContext>
    constexpr typename FormatContext::iterator
      format(const T& e, FormatContext& ctx) const;
};

The specialization is debug-enabled and constexpr-enabled.

template<class FormatContext>
  constexpr typename FormatContext::iterator
    format(const T& e, FormatContext& ctx) const;

Returns: formatter<const char*, char>::format( static_cast<const exception&>(e).what(), ctx).

5. Implementation experience

A formatter for std::exception itself, as well as types derived from it, is available in the open-source {fmt} library ([FMT]).

References

Non-Normative References

[FMT]
Victor Zverovich; et al. The {fmt} library. URL: https://github.com/fmtlib/fmt
[LWG4087]
Victor Zverovich. LWG Issue 4087: Standard exception messages have unspecified encoding. URL: https://cplusplus.github.io/LWG/issue4087
[P2319]
Victor Zverovich. Prevent path presentation problems. URL: https://wg21.link/P2319R5