[boost][review] Boost.Outcome.v2 Review Report
# Boost.Outcome.v2 REVIEW REPORT In conclusion to the review for Boost.Outcome(v2) (19-Jan to 28-Jan, 2018): The library is ACCEPTED (conditions below). Acceptance is principally based on: (1) Reviewers found the library useful to address a current recognized need (2) New idioms are enabled that reviewers found compelling (3) The submission satisfies all Boost Library Requirements Concerns over accepting the library raised in the review discussion include: (a) New idioms proposed are not compelling (b) Requires C++14 (prefer compatibility with C++11) (c) Library may evolve beyond the current design (d) Library is unnecessarily complex Reviews submitted during the review period: *- Vinícius dos Santos Oliveira -- ACCEPT (Fri-26-Jan-2018) *- Andrzej Krzemienski -- ACCEPT (CONDITIONAL) (Fri-26-Jan-2018) *- Bjorn Reese -- ACCEPT (CONDITIONAL) (Sun-28-Jan-2018) *- Daniela Engart -- ACCEPT (Sun-28-Jan-2018) *- John P Fletcher -- ACCEPT (CONDITIONAL) (Sun-28-Jan-2018) In addition, after the review period concluded (but within the duration had the review period been extended) further reviews were submitted: *- Vinnie Falco -- REJECT (Tue-30-Jan-2018) *- Emil Dotchevski -- REJECT (Tue-30-Jan-2018) *- Glen Fernandes -- REJECT (Tue-30-Jan-2018) *- Paul A Bristow -- WOULD ACCEPT (?not a review?) (Wed-31-Jan-2018) *- Rob Stewart -- NOT ACCEPT (Sat-03-Feb-2018) Conditions for acceptance: It is expected: *- Changes are made to use Boost-standard names for macros and namespaces *- Docs are updated to be clear regarding: *- how library treats default-constructed values *- salient attributes of objects, and how “spare_storage” is treated *- Documentation is integrated into Boost framework and release process *- Library is distributed under the Boost Software License (BSL) The remainder of this report contains context and analysis contributing to this decision, including basis for why acceptance purports to be constructive and beneficial to the Boost community, and broader C++ community. # MOTIVATION: REAL-WORLD USE TODAY The prime motivation for acceptance is: *- Reviewers have real-world use cases _today_ for which they found Outcome to be an effective and best available alternative; and which is consistent with current-need and expectations; and which is consistent with ongoing C++ Standard evolution efforts.
From the Library Author: <quote> “Outcome is really an abstraction layer for setting per-namespace rules for when to throw exceptions. Exception throwing is absolutely at the heart of Outcome. That's why Outcome != Expected, and why it ICEs older compilers, and why C++ 14 is needed.”
However it looks like Outcome provides a solution for most of the
Exhibited behavior is two-fold:
(1) Expected errors are handled locally with low overhead (e.g.,
deterministic / predictable with low-latency)
(2) Unexpected errors are type-erased into an 'exception_ptr' and pushed
up the call stack (e.g., exception-throw stack-unwind)
For example, server-side code that is expected to handle a lot of failures,
where “stop-the-world” is never suitable, may always handle errors
locally. In contrast, most other system-wide code may 'throw' an error,
which should be handled within some caller-context.
# AT ISSUE: DESIGN AND IDIOMS
What appears to be debated in this review are the Outcome library design
and idioms; and not the quality of implementation (although the
implementation is criticized as complex -- see below). For example, even
reviewers that voted to reject commented that the library seems sound, and
seems useful for some cases.
This merits repeating: This review has the highly positive characteristic
(despite the accompanying discomfort) of debating the idioms that challenge
what already exists, and which forces re-evaluation of our historical
approaches.
Many of these discussions might otherwise be summarized (and which has a
fair chance of being agreed upon by all parties) as:
*- Error handling can be done with exceptions, or with branch-testing on
error instances;
and some algorithms or constraints may favor one over the other for
technical or
compositional reasons.
It is this highly pragmatic observation that is at the core of the Outcome
library submission.
Outcome enables a new idiom consistent with other pattern explorations
deemed by the C++ community as useful, as demonstrated through acceptance
into the C++ standard for 'std::optional<>' and the pending
'std::expected<>'. Outcome enables value-transport of a multi-modal data
object '
cases, while leaving the general case unsolved. Boost.Outcome is a set of tools (rather than just one) and you are expected to choose one that best solves your particular problem.
Quoting response by library author:
I was just about to say the same thing, but more pointed. <...> Knowing that a piece of code will never, ever see stack unwinding lets you skip handling stack unwinding. Hence `noexcept`.
Furthermore, unlike with exception specifications which were unhelpfully checked at runtime, Outcome fails at compile time. .... You can't successfully compile code if your E types don't have interop specified for them.
Outcome is of particular use in a lower-level layer of code (closer to bare metal, or in server contexts), where explicit deterministic error handling warrants increased tedium or inconvenience in explicit checks for success/failure.
An observer might note that the reviewer (voting to reject) and library
author have a shared view of what the library proposes to do; but the
reviewer desires a generalized solution, rather than an interop-solution
that provides specific performance constraints and behavioral guarantees.
They agree on what it does. They disagree regarding the value and
likelihood of a future possible generalized solution to perform a similar
function, which is not currently proposed for review.
The (perhaps unstated) concept is that no generalized solution may be
possible with today's language, or perhaps ever: Both 'error' objects and
'exception' throws fundamentally serve different use cases:
(a) 'error / status' instance (i.e., “opt-in”): A discrete object that
can be inspected (or ignored), such as to perform conditional processing
(b) 'exception' throw (i.e., “opt-out”): A control transfer-to-caller
that avoids accidental instruction execution (e.g., stack-unwind) and which
cannot be ignored
Proponents of (a) talk of contract and execution simplicity, and of
determinism / efficiency. Proponents of (b) talk of composition
simplicity, and avoidance of edge cases due to liberation from local tedium.
A further complication is due to how arbitrary (implementation-specific)
data is returned for failure handling: It can be encoded into the type
system (increasing coupling across APIs), or may be type-erased (such as
done by 'std::error_code'). Noted in the review is that 'exception' throws
are a special case of type erasure, as the C++ runtime performs the type
erasure without impacting the declared API (thus providing a very large
part of the convenience for using exceptions).
The Outcome library attempts to unify '
Boost.Outcome is not to “replace exception handling in your programs”: It is used to cover those isolated places where exception handling proves inferior to manual control flows.
From the library author: As the last section of the tutorial covers, there is a non-source-intrusive mechanism for externally specifying interoperation rules to handle libraries using one policy interoperating with libraries with different policies. This lets Eve stitch together the Alice and Bob libraries without having to modify their source code, and without affecting any other libraries. I personally think this Outcome's coup de grace and why it's the only scalable choice for large programs considering using
We might now expand our Matrix Of Confusion for preferring 'error' or
'exception' to include:
(a) “Generalized-pattern” vs. “Localized-reasoning”
(b) Expected vs. Non-Expected failure
(c) Dependency upon subsystem providing 'error' vs. subsystem providing
'exception'
While technically a “matrix”, commonly this is (quite) multi-dimensional:
Frequently we depend upon many subsystems, each of which make very
different decisions for how they relay disappointment, and where each
individual subsystem will *change* that decision in surprising ways merely
when we perform a version update. This is the brittleness that Outcome
intends to address.
Outcome is merely a mechanism to enable 3rd party module inter-operation.
It does not make the decision for whether 'error' or 'exception' instances
are provided by some subsystem, nor does it care. Rather, it is a
unification mechanism that enables an algorithm to be _authored_ with
specific performance and behavioral guarantees when reliance is upon
one-or-more modules that _made_ that 'error' vs. 'exception' decision in a
manner your specific use case finds unfortunate.
Lastly, reviewers most critical of Outcome point to 'exceptions' as the C++
language mechanism to be leveraged in design, such as to enforce strong
guarantees of object invariants (e.g., RAII permits ctors to throw to
ensure invariants are not violated), and thus the Outcome library is not
needed. However, despite this language feature, alternative idioms such as
private ctors and 'make_xxx()' factory functions are not uncommon to also
enable successful instantiation with internal state upholding invariant
values; and we again must concede that modules employ differing decisions
to compile with exception handling enabled. Thus, it seems reasonable that
a library such as Outcome might exist to present a unifying interface
across 3rd party code with differing design decisions, rather than hope for
a simpler world where a single (exception-based) approach is mandated
system-wide across modules.
# CONCERN: CUSTOMIZATION POINTS (COMPLEXITY)
Outcome attempts to bridge vocabulary types for '
error handling.
In this context where Outcome may possibly exhibit minimal sufficient
complexity to address its intended target of cross-module
'
“The complexity of software is an essential property, not an accidental one. Hence, descriptions of a software entity that abstract away its complexity often abstracts away its essence.” -- Fred Brooks, “No Silver Bullet” (1986)
# DISAGREEMENT: REQUIRES C++14 (versus C++11)
Outcome requires C++14 and is identified as failing on some toolchains
exhibiting non-conforming C++ Standard behavior. The review raised
concerns that requiring C++14 (rather than C++11) would limit the library's
suitability for Boost inclusion.
In this context, it might be suggested that an error variant is needed by
everyone, so we might reject a C++14 implementation in the hope that a
future C++11 compatible library will be proposed.
It is noted that Outcome v1 supported old compilers back to clang 3.1 and
gcc 4.9, and had many workarounds to suppress non-conforming compiler
behavior; and used preprocessor metaprogramming to work around compile-time
selected CRTP (because of compile-time costs, and issues on older
compilers). However, these approaches were rejected in the v1 review as
too complex; and reviewers were not persuaded by concerns raised by the
library author that their removal would demand dropping compatibility with
older toolchains. Outcome v2 removed these workarounds based on v1
feedback, resulting in a requirement for newer toolchains.
Similar discussions have been raised in other contexts regarding Boost
distribution packaging, and the possibility of forking “pre/post” C++11
(e.g., “modern C++”) libraries into separate Boost distributions. This is
an unfortunately complex issue, as Boost already contains libraries
supporting varying levels of “minimum” requirements including varying
support for compilers exhibiting non-conforming behavior for C++98, C++03,
C++11, C++14, and C++17 (and others). Some libraries such as Boost.Hana or
those reliant upon heavy 'constexpr' behavior demand the very-latest C++17
toolchains.
Further, it is recognized that in some cases backward-compatibility can be
undesirable, or fundamentally limiting to the library's usefulness. For
example, at a recent 'BoostCon' / 'C++Now' conference an attempt was made
to evolve the 'Boost.Date_Time' API to use the seemingly highly suitable
C++17 'structured binding' declaration to decompose 'date / time' objects.
Unfortunately, this work was abandoned: Due to use by other Boost
libraries restricted to a previous C++ Standard, and the discovery that
'Boost.Date_Time' library semantics fundamentally changed when using this
new language feature, it was concluded to not be feasible to support both
the legacy API and new API using structured bindings. It appears in this
case a new library must be authored without consideration of
backward-compatibility to enable this evolution for what appears to be an
otherwise obvious or natural expressiveness to decompose 'date / time'
objects using 'structured binding' declarations.
It is noted that C++11 is already eclipsed by C++14 and C++17, and that
current Boost library requirements merely require C++ Standard conforming
behavior, whereby the library must clearly document those platforms that
are supported. Further, it is noted that a v1 implementation supporting
older tool chains was rejected (so v2 now requires C++14). As this is a
complex issue that demands consideration of many tradeoffs (including
feasibility and behavior fundamental to the library itself), it is expected
that the Library Maintainer constantly review and evolve these decisions as
the C++ language evolves; toolchains are updated; and users raise issues or
provide contributions.
# CONCERN: LIBRARY FUTURE EVOLUTION
A concern is raised that upon acceptance into Boost, Outcome may evolve
beyond the design reviewed for acceptance. Further, a specific concern was
raised regarding Outcome exploring evolution of an 'std::error_code' (which
we might call a proposed 'std2::error_code').
Within the context of Outcome, the 'error' type exists as a mere
customization point by which the user may supply a bespoke definition, or
incidentally use the 'std::error_code' provided by the C++11 Standard.
Both of these are seen today in common practice. It is expected that users
(and perhaps the library author) will continue to explore possible 'error'
implementations for specific purposes (such as exploration of an 'error'
for small-embedded that does not rely upon allocation machinery). This is
viewed as a necessary and healthy advancement of the science, for which the
library (by design) conveniently empowers the end-user to parameterize
domain-specific 'error' types into 'result<>' or 'outcome<>'.
Indeed, it is hoped (and expected) that a greater amount of
user-experimentation or flirtation with 'error' value transfer will be
performed (not less) in the context of user-specific needs or engineering
constraints: This is a fundamental benefit from having Outcome as a design
option for authoring interfaces across module boundaries.
Regarding a possible future “drift” from its clearly-stated mission, this
review considers the library as submitted; and defers to the Boost
community regarding policies and procedures for handling libraries included
in the Boost distribution (which exist in varying states of evolution and
maintenance).
# FINAL THOUGHTS
The Boost community is stronger for enabling and exploring idioms, and for
not shying away from the difficult (and sometimes contentious) effort to
discover new approaches that address the dark corners that stress our
real-world systems. These are (perhaps) the only noble efforts that
someday may lead to new best practices.
If experience (otherwise known as, “painful memory of limitation”) is a
prime driver for the design and implementation choices made by developers,
then it is unreasonable in a review such as this to expect agreement on all
fronts. Indeed, our systems have differing constraints and evolutionary /
scaling prospects, and we fear different things. However, it seems
important to keep in mind that this disagreement continues to be necessary
and expected: Rather than pretending to hide within an echo chamber, the
Boost Community does the “hard work” of challenging perspectives, pushing
the envelope, and questioning assertions in the face of a constantly
shifting technological landscape and evolving C++ language. Dissenting or
critical reviews are essential, and desired.
The long Boost history includes many examples of speculative and risky
approaches, which in hindsight are now considered common and "best"
practice. Few C++ developers today can practice professionally without at
least passing knowledge of template-metaprogramming, which used to be an
esoteric ritual only within the confines of Boost.
Outcome was designed under the proposition that cross-module 'error' /
'exception' handling in today's systems is unnecessarily brittle and
problematic. Reviewers found Outcome to effectively address a serious
concern of transporting different '
participants (1)
-
charleyb123 .