1. Revision History
1.1. Revision 1 - June 17th, 2019
-
This paper is dead. Please see § 2 Final Words for a thorough review of next steps and things that you, the reader, can do to move this area along if that is your desire.
1.2. Revision 0 - October 7th, 2018
-
Initial release.
2. Final Words
This paper was discussed, but did not generate consensus. There are a few ways that this paper can be moved forward in a sensible manner. Here is a (non-exhaustive) list of alternatives with some minor preliminary discussion. None of this represents anyone’s opinions but the author’s.
2.1. Create a full optional < T &>
This was what p1683r0 was supposed to be (now published, but not pursued). However, before submitting p1683r0 the author requested feedback from several Committee members with a lot of experience in the area. It was explained that p1683r0 would die a fiery death at the hands of the Committee and because it would stir the holy war and pick a side in said holy war, and that a gentle, simpler paper would fare much better.
In perhaps a cruel twist of fate, some of the Against and Neutral votes of the final poll was because p1175 -- this paper -- _did not go far enough_ and tried to go for the simple
that covered the majority of the use cases. That was not enough for many people in the room, and thusly faced opposition because of what it tried to do. To paraphrase, a template specialization which changes from
to
is bad in generic code, and is "actively harmful". At this point, individuals cite
for something that "changes the base template".
The author of this paper does agree, but that was the entire point: to create a
type while the decisions were hammered out by a less time-constrained Committee at a later date, then shipped to become a normal type under later rules when individuals decided how a type like
with a reference should behave in the face of assignment and comparison for parity with
. There was almost-consensus to move forward with this simple version, but by not making it through it seems like it would have been a better idea to start and finish the holy war in its entirety by submitting p1683.
Despite publishing this paper so that the work does not go to waste, the author of this paper and p1683 formally abstains from pursuing this area further. It is important to note that without support for references, performance pitfalls will be a common sort of headache when working with optionals, especially optionals that decay return types. Fixing that divergence is critical to solving the problem on hand, and fixing generic code that does not have 2 separate types for what is the same conceptual model -- references or not -- is also important.
Maybe a full
will survive, but it should be noted that the last time a decision about a reference type in a vocabulary type (
) managed to slip its way through the committee and almost made it in, it was pulled at the eleventh hour because the semantics were not believed to be correct. Winning this war is not a Goliath that can be taken down easily anymore: God’s Greatest Speed to whoever picks up this slingshot and any of the 4 sides in this battle.
2.2. Create optional_ref < T >
This is taking this proposal, and giving it a new type name as
. This does not solve the problems of generic code that needs to chain properly, nor does it help with teaching (if you have a value, use this, but if you have a reference, use this other type, and be careful about ...). Library developers will need to wrap their propagating code with
. But at this point, why not just forcefully make the library developers do the same thing except without
and with more
? Code will be uglier, certainly, and library developers will have to virally sprinkle it at all the critical junctures of their code and introduce the same
calls so many libraries already contain, but at least the regular user’s use cases will be supported. In fact, it can just be
. This rarely shows up in code because its usability is poor and requires extra syntax just to access the value inside. It is also entirely unclear whether or not this optional should be spaced-optimized (it isn’t).
It is the author’s opinion that this is a hack. It does not really help generic code that needs to propagate a basic C++ reference-qualified type -- which is used prodigiously in return values -- through many higher order abstractions. It is the simple solution but without any aspirations to unite or fix the community, just continue C++'s legacy of patchwork without true solution. Work in this direction is strongly discouraged for standardization: we can and should do better than patchwork.
2.3. Create a second optional type that gets the semantics right, std :: maybe < T >
will hold references and values. It will play nice because the semantics can be done properly from the ground up. It is a workable solution, but someone will need to put in the time and convince everyone involved this is a good solution. In the face of
already being in the standard, the author does not know what to think about this solution. New types are attractive land grabs for those wishing to leave the baggage behind, but this means that we still have a problem with
and
which need answers so we -- as a community -- can stop replicating this conundrum.
In the end,
is at best a door to making Standard Library maintainers and friends' lives miserable with duplicated effort. At worst, it is a siren’s song into splintering the community in yet another horrible, incompatible way.
2.4. Just use pointers.
This is the worst of all the ideas presented. Pointers are not optionals, they are confusing in interfaces, and require programmers to read documentation rather than have the constraints of their type be communicated at compile-time. Pointers are sufficient for C-style interfaces that are optional, and even then are dubious at best when used in string interfaces for C (see the
header’s conversion functions and the ambiguity of pointers there, or LLVM’s owner-nonowner-optional problems that they have been trying to clean up for 3+ years).
Pointers are bad reference optionals, as both parameters and return types. They change the syntax necessary to work with a type and make it difficult to work with generic code.
2.5. What about variant
, expected
, etc.?
The author, sincerely, does not have an answer for the reader here.
? More
? It is impossible to know how this will all shape up, especially with a type that is meant to deal with parameters and function returns like
.
Nevertheless, the author of this paper encourages everyone to drop this paper now and go read p1683. Even if you have a differing opinion, it is important that you are informed of the work and efforts done in this space and do not come to the Committee ill-prepared as the author of this paper once did.
Best of luck to you.
3. Overview
Currently | With Proposal |
---|---|
❌ - Compilation error; need to return as a value (and copy)
|
✔️ - Compiles and runs with no copying |
3.1. The Great Big Table of Behaviors
Below is a succinct 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 |
4. 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.
5. 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 p1683r0 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.
5.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 lvalues 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.
6. 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 .
7. Proposed Wording
All wording is relative to [n4762].
7.1. Intent
The intent of this proposal is to provide a lvalue reference optional. The comparison and equality operators will not be provided. The assignment operator from an lvalue reference or from an lvalue reference of its base will not be provided. The copy-assignment from two optionals will rebind the optional.
Comparison to
with equality will provided.
7.2. Feature Test Macro
The proposed feature test macro is
.
7.3. Proposed Library 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 lvalue 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 lvalue reference.
optional < U >
8. 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.