1. Revision History
1.1. Revision 0
Initial release.
2. Overview
2.1. The Great Big Table of Behaviors
Below is a succint synopsis of the options presented in this paper and their comparison with known solutions and alternative implementations. It does not include the totality of the optional API surface, but has the most exemplary pieces. A key for the symbols:
✔️ - Succeeds
🚫 - Compile-Time Error
❌ - Runtime Error
❓ - Implementation Inconsistency (between engaged/unengaged states, runtime behaviors, etc.)
optional behaviors | |||||
---|---|---|---|---|---|
Operation | T | std::reference_wrapper<T> | Proposed: T& conservative | ||
exemplary implementation(s) | ✔️ std::optional nonstd::optional llvm::Optional folly::Optional core::Optional | ✔️ std::optional nonstd::optional llvm::Optional folly::Optional core::Optional | ✔️ std::experimental::optional sol::optional | ||
| ✔️ copy constructs (disengaged: nothing)
| ✔️ binds reference (disengaged: nothing) | ✔️ binds reference (disengaged: nothing) | ||
| ✔️ move constructs (disengaged: nothing)
| ✔️ binds reference (disengaged: nothing) | ✔️ binds reference (disengaged: nothing) | ||
| ✔️ (copy) constructs
| ✔️ binds reference | ✔️ binds reference | ||
| ✔️ (move) constructs
| 🚫 compile-time error | 🚫 compile-time error | ||
engaged | ✔️ overwrites
| ✔️ rebinds data | 🚫 compile-time error | ||
disengaged | ️✔️ overwrites data | ✔️ rebinds data (overwrites reference wrapper) | 🚫 compile-time error | ||
engaged | ✔️ move-assigns
| 🚫 compile-time error | 🚫 compile-time error | ||
disengaged | ✔️ constructs
| 🚫 compile-time error | 🚫 compile-time error | ||
engaged | ✔️ overwrites
| 🚫 compile-time error | 🚫 compile-time error | ||
disengaged | ️✔️ overwrites data | ✔️ overwrites data | 🚫 compile-time error | ||
engaged; arg engaged | ✔️ move assign
| ✔️ rebind data | ✔️ rebind data | ||
disengaged; arg engaged | ✔️ move construct
| ✔️ rebind data | ✔️ rebind data | ||
engaged; arg disengaged | ✔️ disengage
| ✔️ disengage
| ✔️ disengage
| ||
disengaged; arg disengaged | ✔️ nothing | ✔️ nothing | ✔️ nothing | ||
engaged | ✔️ copy assigns
| ✔️ copy assigns
| ✔️ copy assigns
| ||
disengaged | ❌ runtime error | ❌ runtime error | ❌ runtime error | ||
engaged | ✔️ move assigns
| ✔️ move assigns
| ✔️ move assigns
| ||
disengaged | ❌ runtime error | ❌ runtime error | ❌ runtime error | ||
engaged | ✔️ calls
| 🚫 compile-time error | ✔️ calls
| ||
disengaged | ❌ runtime error | ❌ runtime error | ❌ runtime error | ||
| ✔️ compares values if both engaged, returns true if both disengaged, returns false otherwise | ✔️ compares values if both engaged, returns true if both disengaged, returns false otherwise | 🚫 compile-time error | ||
| ✔️ compares values if both engaged, returns true if both disengaged, returns false otherwise | ✔️ compares values if both engaged, returns true if both disengaged, returns false otherwise | 🚫 compile-time error | ||
engaged | ✔️ compares values | ✔️ compares values | 🚫 compile-time error | ||
engaged | ✔️ compares values | ✔️ compares values | 🚫 compile-time error | ||
disengaged | ✔️ returns false | ✔️ returns false | 🚫 compile-time error | ||
disengaged | ✔️ returns false | ✔️ returns false | 🚫 compile-time error |
3. Motivation
Originally,
-- where
denotes the name of a type -- contained a specialization to work with regular references,
. When some of the semantics for references were called into question with respect to assign-through semantics (assign into the value or rebind the optional) and how comparisons would be performed, the debate stopped early and no full consensus was reached. Rather than remove just the operator or modify comparison operators, the entirety of
was removed entirely.
This left many codebases in an interesting limbo: previous implementations and external implementations handled references without a problem. Transitioning to pointers created both a problem of unclear API (pointers are an exceedingly overloaded construct used for way too many things) and had serious implications for individuals who wanted to use temporaries as part of their function calls.
As Library Evolution Working Group Chair Titus Winters has frequently stated and demonstrated, having multiple vocabulary types inhibits growth of the C++ ecosystem and fragments libraries and their developers. This comes at an especially high cost for
,
,
,
and more. There are at least 6 different
s in the wild with very slightly differing semantics, a handful more
s, a few
types, and more (not including the ones from
). Of note is that many optionals have been created and are being nurtured to this day without the need to take care of legacy code, which greatly inhibits interopability between code bases and general sharing.
4. Design Considerations
This solution is the simplest cross-section that enables behavior without encroaching upon a future where the to be posed in the yet-to-be-released p1129r0 will reach an answer to move the C++ community forward. Care has been taken to only approach the most useful subsection, while keeping everything else deleted. This will enable developers to use optional for the 80% use cases, while users handle the 20% use case of rebinding, assigning through, or comparing values / location by choosing much more explicit syntax that will not be deprecated.
4.1. The Solution
This baseline version is the version that has seen adoption from hundreds of companies and users: an optional where
is not allowed, comparison operators are nuked, rebinding is done with an explicit wrapping of
and assign-through is performed using
. This keeps
as a delay-constructed type, allows usage in all of the places a programmer might want to put it trivially, allows it to be used as a parameter, and allows it to be used as a return type.
It forces the user to choose assign-through by explicitly dereferencing the optional as in
, and forces rebind by making the user specify
. It is safe, but penalizes the user for this safety with verbosity (and, arguably, disappointment). It also prevents users of
,
,
and others from migrating painlessly to the standard version, but still allows many of the high-priority uses of such classes with references to transition to using the standard library version.
Another notable feature of adding optional references and using
is the ability to transition codebases that use temporary values (r-values) passed to functions, this solution will work for individuals without requiring a full rewrite of the code. For example, the function
can be transitioned to
and work for both l-values and r-values passed to the type. This is safe thanks to C++'s lifetime rules around temporary objects, when they bind to references, and when they are lifetime extended; see [class.temporary]/6 for applicable lifetime extension clauses of temporaries, including temporaries that bind to stored references.
5. Implementation Experience
This "simple", baseline version is featured in akrzemi/optional, [sol2], and the "portable" version of [boost-optional] (following Boost’s advice to avoid the use of the assignment operator in select cases for compilers with degenerate behavior). It is the least offensive, tasteless, hazard-proof, odorless, non-toxic, biodegradable, organic, and politically correct choice™; it can also be expanded upon at a later date.
This specific version has seen experience for about 8+ years. It is known to be safe and easy to use and covers a good portion of user’s use cases without having to invoke the problem of figuring out assign-through or rebind semantics.
The comparison operators do not exist, which make it a subset of
and other optional implementations. Additional work can be done later once the Committee and its constituents .
6. Wording
All wording is relative to [n4762].
6.1. Intent
The intent of this proposal is to provide a l-value reference optional. The comparison and equality operators will not be provided. The assignment operator from an l-value reference or from an l-value reference of its base will not be provided. The copy-assignment from two optionals will rebind the optional.
Comparison to
with equality will provided.
6.2. Feature Test Macro
The proposed feature test macro is
.
6.3. Proposed Wording
Append to §16.3.1 General [support.limits.general]'s Table 35 one additional entry:
Macro name Value __cpp_lib_optional_ref 201811L
Add additional class template specialization to §19.6.2 Header
synopsis [optional.syn]:
// 19.6.3, class template optional template < class T > class optional ; // 19.6.4, class template optional for lvalue reference types template < class T > class optional < T &> ;
Insert §19.6.4 [optional.lref] after §19.6.3 Class template
[optional.mod]:
19.6.4 Class template optional for lvalue reference types [optional.lref]namespace std { template < class T > class optional < T &> { public : typedef T & value_type ; // 19.6.4.1, construction/destruction constexpr optional () noexcept ; constexpr optional ( nullopt_t ) noexcept ; constexpr optional ( T & ) noexcept ; optional ( T && ) = delete ; constexpr optional ( const optional & ) noexcept ; template < class U > optional ( const optional < U &>& ) noexcept ; ~ optional () = default ; // 19.6.4.2, mutation constexpr optional & operator = ( nullopt_t ) noexcept ; optional & operator = ( optional && ) = delete ; optional & operator = ( const optional & ) = delete ; // 19.6.4.3, observers constexpr T * operator -> () const ; constexpr T & operator * () const ; constexpr explicit operator bool () const noexcept ; template < class U > constexpr T value_or ( U && ) const & ; // 19.6.4.4, modifiers void reset () noexcept ; private : T * ref ; // exposition only }; } // namespace std 1 Engaged instances of
where
optional < T > is of lvalue reference type, refer to objects of type
T , but their life-time is not connected to the life-time of the referred to object. Destroying or disengageing the optional object does not affect the state of the referred to object.
std :: remove_reference_t < T > 2 Member
is provided for exposition only. Implementations need not provide this member. If
ref , optional object is disengaged; otherwise
ref == nullptr points to a valid object.
ref 19.6.4.1 Construction and destruction [optional.lref.ctor]
constexpr optional < T &>:: optional () noexcept ; constexpr optional < T &>:: optional ( nullopt_t ) noexcept ; 1 Effects: Constructs a disengaged optional object by initializing ref with nullptr.
2 Ensures:
bool ( * this ) == false. 3 Remarks: For every object type
these constructors shall be constexpr constructors.
T optional < T &>:: optional ( T & v ) noexcept ; 4 Effects: Constructs an engaged optional object by initializing ref with
.
addressof ( v ) 5 Ensures:
.
bool ( * this ) == true&& addressof ( * ( * this )) == addressof ( v ) optional < T &>:: optional ( const optional & rhs ) noexcept ; template < class U > optional < T &>:: optional ( const optional < U &>& rhs ) noexcept ; 6 Constraints:
, and
is_base_of < T , U >:: value == trueis true.
is_convertible < U & , T &>:: value 7 Effects: If rhs is disengaged, initializes ref with
; otherwise, constructs an engaged object by initializing ref with
nullptr .
addressof ( * rhs ) optional < T &>::~ optional () = default ; 9 Effects: No effect. This destructor shall be a trivial destructor.
19.6.4.2 Mutation [optional.lref.mutate]
optional < T &>& optional < T &>:: operator = ( nullopt_t ) noexcept ; 1 Effects: Assigns ref with a value of
. If ref was non-null initially, the object it referred to is unaffected.
nullptr 2 Returns:
.
* this 3 Ensures:
.
bool ( * this ) == false19.6.4.3 Observers [optional.lref.observe]
T * optional < T &>:: operator -> () const ; 1 Requires:
.
bool ( * this ) == true2 Returns: ref.
3 Throws: nothing.
T & optional < T &>:: operator * () const ; 4 Requires:
.
bool ( * this ) == true5 Returns:
* ref 6 Throws: nothing.
explicit optional < T &>:: operator bool () noexcept ; 7 Returns:
ref != nullptr template < class U > T & optional < T &>:: value_or ( U && u ) const ; 8 Returns:
when
. value () is true, otherwise
bool ( * this ) .
std :: forward < U > ( u ) 19.6.4.4 Observers [optional.lref.modifiers]
template < class U > T & optional < T &>:: value_or ( U && u ) const ; 12 Returns:
when
. value () is true, otherwise
bool ( * this ) .
std :: forward < U > ( u )
Modify §19.6.6 Relational operators [optional.relops] to include the following top-level clause:
1 None of the comparisons in this subsection participate in overload resolutions ifor
T in
U or
optional < T > are an l-value reference.
optional < U >
Modify §19.6.8 Comparison with
[optional.comp_with_t] to include the following top-level clause:
1 None of the comparisons in this subsection participate in overload resolutions ifor
T in
U or
optional < T > are an l-value reference.
optional < U >
7. Acknowledgements
Thank you to sol2 users for encouraging me to fix this in the standard. Thank you to Lisa Lippincott for encouraging me to make this and one other proposal after seeing my C++Now 2018 presentation. Thank you to Matt Calabrese, R. Martinho Fernandes and Michał Dominiak for the advice on how to write and handle a paper of this magnitude.
Thank you to Tim Song and Walter Brown for reviewing one of my papers, and thus allowing me to improve all of them.