Class maybe
Synopsis
#include "breeze/vocabulary/maybe.hpp"
template< typename T, typename Traits = boolean_maybe_traits >
class maybe
Description
A template for "maybe" values.
A class template to represent "possible" values. The idea was signaled by James Kanze, and comes from
John J. Barton, Lee R. Nackman (1994). Scientific and Engineering C++: An Introduction with Advanced Techniques and Examples. Addison-Wesley Professional. ISBN 0-201-53393-6.
The book calls it "Fallible", but we chose a more general name. In type theory terminology, maybe< T >
is an option type. Many languages support this concept, or the related concept of nullable type, directly.
Basically: maybe< T >
is used as return type for functions that logically return a T
or fail. The maybe< T >
object keeps track of its validity state, and any attempt to retrieve the T
object when it is marked as invalid will cause an assertion failure.
Note that the original Barton and Nackman solution threw an exception, instead.
The Breeze library guarantees that the T
object, if any, is stored as a part of its maybe
object: no additional storage is used.
There are other important differences compared to the version provided by Barton and Nackman.
- At least for the moment, there is no
invalidate()
function; I'm still waiting to see if a reasonable usage for it exists (perhaps a cache?). - No conversion function to
T
is provided. Among other things, it wouldn't be useful in at least the following situations:- when you want to invoke a member function on the "real" object:
(but, of course, the latter is stylistically rare: one would usually assign the function return value first)maybe< std::string > my_function() ; my_function().size() ; // error my_function().value().size() // OK
- when
T
itself has a user defined conversion, which you want to be applied
value()
. Curiously enough, Barton and Nackman's book introducesFallible
as an example of using conversions ("to add a binary state"—valid or invalid—and checking to objects). The conversion itself, however, isn't part of the concept: it just makes the checking more "transparent" (at the well-known cost that implicit conversions generally bring). - when you want to invoke a member function on the "real" object:
- It isn't required for
T
to have a default constructor. - Has a richer interface and supports move semantics.
- Has an additional template parameter (
Traits
) which allows specifying several invalid states or several valid states.
The template parameters are:
T
The type of the value to store when themaybe
is valid. As usual, its move constructor(s) and move assignment operator(s), if any, must not emit exceptions.Traits
A traits class defining the valid and the invalid states of themaybe
object. It shall contain:- a nested type or typedef named "status" that can be used to store the state of the
maybe
object - an
is_valid
() static function, which returns whether a given value of typestatus
corresponds to a valid status or not - two static functions, named "default_invalid" and "default_valid" which give the default invalid and valid state, respectively
Traits::status
must not emit exceptions (note that this is a stronger requirement than the one onT
, which concerns move only).- a nested type or typedef named "status" that can be used to store the state of the
- A final note about std::optional
std::optional
, which has the same purpose as our maybe
.
Generally, I'm against using components that do double duty with the standard library, but std::optional:
- is IMHO quite over-engineered
- abuses operator overloading to provide a pointer-like syntax for unchecked access to the contained value
- has an error-prone conversion to
bool
instead of anis_valid()
function: seestd::optional< bool >
- treats a valid
std::optional< T >
as aT
in some contexts (comparison operators) and not in others - doesn't (yet?) support a generalized error status, which is essential for our usages
So, I'm not going to replace our maybe
with std::optional
, even in C++17.
Methods
maybe overload | Constructs an invalid maybe . | |
maybe overload | Constructs a valid maybe . | |
~maybe | ||
default_to | Returns: value() if is_valid() ; otherwise default_value . | |
is_valid | Returns: Traits::is_valid( status() ) | |
operator= overload | ||
status | Returns: The validity status. | |
value | Returns: A reference to the T value. |
Source
Lines 191-365 in breeze/vocabulary/maybe.hpp.
template< typename T, typename Traits = boolean_maybe_traits >
class maybe
{
public:
typedef T value_type ;
typedef typename Traits::status
status_type ;
// These static_asserts are ugly but... better having them.
// They might be a bit less unpleasant in C++17, if we remove
// the empty message. [FUTURE]
//
// Note: the static_assert on the move assignment of value_type
// has been excluded to cope with the case value_type ==
// std::string. Because, in that case, the static_assert might
// pass or not depending on the implementation (there was, in
// fact, a DR on the noexcept specification of basic_string's
// move assignment operator---LWG 2063---but it was only
// resolved for C++17; formally, in C++11 and C++14, the
// operator should have an unconditional noexcept, despite that
// being known as incorrect in virtue of that DR, but
// implementations vary).
//
// We leave the static_assert commented out, as a reminder to
// enable it when we'll require at least C++17. [FUTURE]
// -----------------------------------------------------------------------
static_assert( std::is_nothrow_move_constructible< value_type >::value,
"" ) ;
// static_assert( std::is_nothrow_move_assignable< value_type >::value,
// "" ) ;
static_assert( std::is_nothrow_copy_constructible< status_type >::value,
"" ) ;
static_assert( std::is_nothrow_copy_assignable< status_type >::value, "" ) ;
static_assert( std::is_nothrow_move_constructible< status_type >::value,
"" ) ;
static_assert( std::is_nothrow_move_assignable< status_type >::value, "" ) ;
//! Constructs an invalid \c maybe.
//!
//! \pre
//! ! Traits::is_valid( status )
//!
//! \post
//! - ! is_valid()
//! - status() == status
// -----------------------------------------------------------------------
explicit maybe( status_type status =
Traits::default_invalid() ) ;
//! Constructs a valid \c maybe.
//!
//! \pre
//! Traits::is_valid( status )
//!
//! \param value
//! The \c T value to copy.
//!
//! \param status
//! The status to associate to the object.
//!
//! \post
//! - is_valid()
//! - value() refers to a copy of value
//! - status() == status
// -----------------------------------------------------------------------
explicit maybe( T const & value, status_type status =
Traits::default_valid() ) ;
//! \pre
//! Traits::is_valid( status )
//!
//! \param value
//! The \c T value to move from.
//!
//! \param status
//! The status to associate to the object
//!
//! \post
//! - is_valid()
//! - value() is moved from value
//! - status() == status
// -----------------------------------------------------------------------
explicit maybe( T && value, status_type status =
Traits::default_valid() ) ;
//! \post
//! - ! is_valid() || value() refers to a copy of
//! other.value()
//! - status() == other.status()
// -----------------------------------------------------------------------
maybe( maybe const & other ) ;
//! \post
//! - value() is moved from other.value()
//! - status() == other.status()
// -----------------------------------------------------------------------
maybe( maybe && other ) noexcept ;
~maybe() noexcept ;
//! \post
//! - ! is_valid() || value() refers to a copy of
//! other.value()
//! - status() == other.status()
// -----------------------------------------------------------------------
maybe & operator =( maybe const & other ) ;
//! \post
//! - ! is_valid() || value() is moved from other.value()
//! - status() == other.status()
// -----------------------------------------------------------------------
maybe & operator =( maybe && other ) noexcept ;
//! \param value
//! The value to copy.
//!
//! \post
//! - is_valid()
//! - value() refers to a copy of value
//! - status == Traits::default_valid()
// -----------------------------------------------------------------------
maybe & operator =( T const & value ) ;
//! \post
//! - is_valid()
//! - value() is moved from value
//! - status() == Traits::default_valid()
// -----------------------------------------------------------------------
maybe & operator =( T && value ) noexcept ;
//! \return
//! The validity status.
// -----------------------------------------------------------------------
status_type status() const noexcept ;
//! \return
//! Traits::is_valid( status() )
// -----------------------------------------------------------------------
bool is_valid() const noexcept ;
//! \return
//! A reference to the \c T value.
//!
//! \pre
//! is_valid()
// -----------------------------------------------------------------------
T const & value() const ;
//! \return
//! \c value() if \c is_valid(); otherwise \c default_value.
//!
//! Note that, differently from \c value(), this function
//! returns by value, which prevents problems of dangling
//! references. Note, too, that, for this reason, it is not
//! noexcept (\c T's copy constructor might throw); a
//! conditional noexcept() would be an option, but it adds too
//! much complexity, in our opinion, and of course we would have
//! to use it consistently, not just for \c %default_to().
// -----------------------------------------------------------------------
T default_to( T const & default_value ) const ;
private:
void construct( T const & value ) ;
void construct( T && value ) noexcept ;
void destroy() noexcept ;
T & non_const_value() ;
alignas( T ) unsigned char
m_storage[ sizeof( T ) ] ;
status_type m_status ;
} ;