8
\$\begingroup\$

I wrote an Optional<T> implementation in C++14 in order to support pre-C++17 compiler.

Differences to std::optional<T>:

  • My implementation is not explicit-correct; Optional<T> to Optional<U> conversion is always explicit even when T to U conversion can be done implicitly. Similarly, U&& to Optional<T> conversion is always implicit. Getting this correct requires a lot of SFINAE hackery which I'm not particularly fond of.

  • My implementation presupposes .HasValue() == true on access and generates UB if this requirement is not met. Hence, something like std::bad_optional_access does not exist.

  • My implementation doesn't support std::initializer_list because I disagree with std::initializer_list and don't use it anywhere in my project.

I would greatly appreciate somebody else having a look at my code. I'm particularly unsure about the std::is_ traits and the noexcept qualifiers.

Code:

#if __cplusplus == 201703L
    #define SYMMAP_CPP17 1
#endif

#include <type_traits>
#include <utility>
#include <algorithm>
#include <functional>
#include <new>

#ifdef SYMMAP_CPP17
    #define SYMMAP_LAUNDER( ... ) ( std::launder( __VA_ARGS__ ) )
#else
    #define SYMMAP_LAUNDER( ... ) ( __VA_ARGS__ )
#endif

namespace Symmap {
    struct NullOptT {
    };
    static constexpr NullOptT NullOpt;

    struct InPlaceT {
    };
    static constexpr InPlaceT InPlace;

    template< typename T >
    class Optional;

    template< typename T >
    struct IsOptional : public std::false_type {
    };
    template< typename T >
    struct IsOptional< Optional< T > > : public std::true_type {
    };

    template< typename T >
    constexpr bool IsOptionalV = IsOptional< T >::value;

    template< typename T >
    class Optional {
        static_assert( !std::is_lvalue_reference< T >::value,   "T may not be a reference type" );
        static_assert( !std::is_rvalue_reference< T >::value,   "T may not be a reference type" );
        static_assert( !std::is_const< T >::value,              "T may not have top-level CV qualifiers" );
        static_assert( !std::is_volatile< T >::value,           "T may not have top-level CV qualifiers" );
        static_assert( !IsOptionalV< T >,                       "T may not be an Optional" );

    public:
        using ValueType = T;

    private:
        using StorageT = std::aligned_storage_t< sizeof( ValueType ), alignof( ValueType ) >;
        StorageT mStorage;
        bool mHasValue = false;

        void* GetStorage() noexcept
        {
            return reinterpret_cast< void* >( &mStorage );
        }
        template< typename... ArgsT >
        void Construct( ArgsT&&... Args ) noexcept( std::is_nothrow_constructible< ValueType, ArgsT... >::value )
        {
            static_assert( std::is_constructible< ValueType, ArgsT... >::value, "ValueType cannot be constructed from ArgsT..." );

            assert( !HasValue() );
            new( GetStorage() ) ValueType( std::forward< ArgsT >( Args )... );
            mHasValue = true;
        }

        /*
         * We use a constexpr boolean instead of noexcept( Optional::Destruct() )
         * Workaround for a longstanding GCC bug:
         *   https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52869
         */
        static constexpr bool NoexceptDestructible = std::is_nothrow_destructible< ValueType >::value;
        void Destruct() noexcept( NoexceptDestructible )
        {
            if( HasValue() )
                Value().ValueType::~ValueType();
        }

    public:
        Optional() noexcept = default;
        Optional( NullOptT ) noexcept
        {
        }

        Optional( Optional const& Rhs ) noexcept( std::is_nothrow_copy_constructible< ValueType >::value )
        {
            static_assert( std::is_copy_constructible< ValueType >::value, "ValueType cannot be copy-constructed" );

            if( Rhs.HasValue() )
                Construct( Rhs.Value() );
        }
        Optional( Optional&& Rhs ) noexcept( std::is_nothrow_move_constructible< ValueType >::value )
        {
            static_assert( std::is_move_constructible< ValueType >::value, "ValueType cannot be move-constructed" );

            if( Rhs.HasValue() )
                Construct( std::move( Rhs.Value() ) );
        }

        template< typename U >
        explicit Optional( Optional< U > const& Rhs ) noexcept( std::is_nothrow_constructible< ValueType, U const& >::value )
        {
            static_assert( std::is_constructible< ValueType, U const& >::value, "ValueType cannot be constructed from U const&" );

            if( Rhs.HasValue() )
                Construct( Rhs.Value() );
        }
        template< typename U >
        explicit Optional( Optional< U >&& Rhs ) noexcept( std::is_nothrow_constructible< ValueType, U&& >::value )
        {
            static_assert( std::is_constructible< ValueType, U&& >::value, "ValueType cannot be constructed from U&&" );

            if( Rhs.HasValue() )
                Construct( std::move( Rhs.Value() ) );
        }

        template< typename... ArgsT >
        Optional( InPlaceT, ArgsT&&... Args ) noexcept( std::is_nothrow_constructible< ValueType, ArgsT... >::value )
        {
            static_assert( std::is_constructible< ValueType, ArgsT... >::value, "ValueType cannot be constructed from ArgsT..." );

            Construct( std::forward< ArgsT >( Args )... );
        }

        template< typename U = ValueType >
        Optional( U&& Arg, std::enable_if_t< std::is_constructible< ValueType, U >::value >* = nullptr ) noexcept( std::is_nothrow_constructible< ValueType, U >::value )
        {
            // static_assert( std::is_constructible< ValueType, U >::value, "ValueType cannot be constructed from U" );

            Construct( std::forward< U >( Arg ) );
        }

        ~Optional() noexcept( NoexceptDestructible )
        {
            Destruct();
        }

        Optional& operator=( NullOptT ) noexcept( NoexceptDestructible )
        {
            Reset();
            return *this;
        }

        Optional& operator=( Optional const& Rhs ) noexcept( NoexceptDestructible && std::is_nothrow_copy_assignable< ValueType >::value && std::is_nothrow_copy_constructible< ValueType >::value )
        {
            static_assert( std::is_copy_assignable< ValueType >::value, "ValueType cannot be copy-assigned" );
            static_assert( std::is_copy_constructible< ValueType >::value, "ValueType cannot be copy-constructed" );

            if( Rhs.HasValue() ) {
                if( HasValue() )
                    Value() = Rhs.Value();
                else
                    Construct( Rhs.Value() );
            } else {
                Reset();
            }
            return *this;
        }
        Optional& operator=( Optional&& Rhs ) noexcept( NoexceptDestructible && std::is_nothrow_move_assignable< ValueType >::value && std::is_nothrow_move_constructible< ValueType >::value )
        {
            static_assert( std::is_move_assignable< ValueType >::value, "ValueType cannot be move-assigned" );
            static_assert( std::is_move_constructible< ValueType >::value, "ValueType cannot be move-constructed" );

            if( Rhs.HasValue() ) {
                if( HasValue() )
                    Value() = std::move( Rhs.Value() );
                else
                    Construct( std::move( Rhs.Value() ) );
            } else {
                Reset();
            }
            return *this;
        }

        template< typename U >
        Optional& operator=( Optional< U > const& Rhs ) noexcept( NoexceptDestructible && std::is_nothrow_assignable< ValueType, U const& >::value && std::is_nothrow_constructible< ValueType, U const& >::value )
        {
            static_assert( std::is_assignable< ValueType, U const& >::value, "ValueType cannot be assigned U const&" );
            static_assert( std::is_constructible< ValueType, U const& >::value, "ValueType cannot be constructed from U const&" );

            if( Rhs.HasValue() ) {
                if( HasValue() )
                    Value() = Rhs.Value();
                else
                    Construct( Rhs.Value() );
            } else {
                Reset();
            }
            return *this;
        }
        template< typename U >
        Optional& operator=( Optional< U >&& Rhs ) noexcept( NoexceptDestructible && std::is_nothrow_assignable< ValueType, U&& >::value && std::is_nothrow_constructible< ValueType, U&& >::value )
        {
            static_assert( std::is_assignable< ValueType, U&& >::value, "ValueType cannot be assigned U&&" );
            static_assert( std::is_constructible< ValueType, U&& >::value, "ValueType cannot be constructed from U&&" );

            if( Rhs.HasValue() ) {
                if( HasValue() )
                    Value() = std::move( Rhs.Value() );
                else
                    Construct( std::move( Rhs.Value() ) );
            } else {
                Reset();
            }
            return *this;
        }

        template< typename U >
        std::enable_if_t< std::is_constructible< ValueType, U >::value && std::is_assignable< ValueType, U >::value, Optional& > operator=( U&& Rhs ) noexcept( std::is_nothrow_constructible< ValueType, U >::value && std::is_nothrow_assignable< ValueType, U >::value )
        {
            // static_assert( std::is_assignable< ValueType, U >::value, "ValueType cannot be assigned U" );
            // static_assert( std::is_constructible< ValueType, U >::value, "ValueType cannot be constructed from U" );

            if( HasValue() )
                Value() = std::forward< U >( Rhs );
            else
                Construct( std::forward< U >( Rhs ) );
            return *this;
        }

        void Reset() noexcept( NoexceptDestructible )
        {
            if( HasValue() ) {
                Destruct();
                mHasValue = false;
            }
        }

        template< typename... ArgsT >
        ValueType& Emplace( ArgsT&&... Args ) noexcept( NoexceptDestructible && std::is_nothrow_constructible< ValueType, ArgsT... >::value )
        {
            static_assert( std::is_constructible< ValueType, ArgsT... >::value, "ValueType cannot be constructed from ArgsT..." );

            Reset();
            Construct( std::forward< ArgsT >( Args )... );
            return Value();
        }

        bool HasValue() const noexcept
        {
            return mHasValue;
        }
        explicit operator bool() const noexcept
        {
            return HasValue();
        }

        ValueType& Value() & noexcept
        {
            assert( HasValue() );
            return static_cast< ValueType& >( *SYMMAP_LAUNDER( reinterpret_cast< ValueType* >( &mStorage ) ) );
        }
        ValueType const& Value() const& noexcept
        {
            assert( HasValue() );
            return static_cast< ValueType const& >( *SYMMAP_LAUNDER( reinterpret_cast< ValueType const* >( &mStorage ) ) );
        }
        ValueType&& Value() && noexcept
        {
            assert( HasValue() );
            return static_cast< ValueType&& >( *SYMMAP_LAUNDER( reinterpret_cast< ValueType* >( &mStorage ) ) );
        }
        ValueType const&& Value() const&& noexcept
        {
            assert( HasValue() );
            return static_cast< ValueType const&& >( *SYMMAP_LAUNDER( reinterpret_cast< ValueType const* >( &mStorage ) ) );
        }

        template< typename U >
        ValueType ValueOr( U&& Fallback ) const& noexcept( std::is_nothrow_copy_constructible< ValueType >::value && std::is_nothrow_constructible< ValueType, U >::value )
        {
            static_assert( std::is_copy_constructible< ValueType >::value, "ValueType cannot be copy-constructed" );
            static_assert( std::is_constructible< ValueType, U >::value, "ValueType cannot be constructed from U" );

            if( HasValue() )
                return Value();
            else
                return ValueType( std::forward< U >( Fallback ) );
        }
        template< typename U >
        ValueType ValueOr( U&& Fallback ) && noexcept( std::is_nothrow_move_constructible< ValueType >::value && std::is_nothrow_constructible< ValueType, U >::value )
        {
            static_assert( std::is_move_constructible< ValueType >::value, "ValueType cannot be move-constructed" );
            static_assert( std::is_constructible< ValueType, U >::value, "ValueType cannot be constructed from U" );

            if( HasValue() )
                return std::move( Value() );
            else
                return ValueType( std::forward< U >( Fallback ) );
        }

        const ValueType* operator->() const noexcept
        {
            return &Value();
        }
        ValueType* operator->() noexcept
        {
            return &Value();
        }
        ValueType const& operator*() const& noexcept
        {
            return Value();
        }
        ValueType& operator*() & noexcept
        {
            return Value();
        }
        ValueType const&& operator*() const&& noexcept
        {
            return Value();
        }
        ValueType&& operator*() && noexcept
        {
            return Value();
        }

        void Swap( Optional& Rhs ) noexcept( std::is_nothrow_move_constructible< ValueType >::value
#ifdef SYMMAP_CPP17
            && std::is_nothrow_swappable< ValueType >::value
#endif
            )
        {
            using std::swap;

            static_assert( std::is_move_constructible< ValueType >::value, "ValueType cannot be move-constructed" );
#ifdef SYMMAP_CPP17
            static_assert( std::is_swappable< ValueType >::value, "ValueType cannot be swapped" );
#endif

            if( Rhs.HasValue() ) {
                if( HasValue() ) {
                    swap( Value(), Rhs.Value() );
                } else {
                    Construct( std::move( Rhs.Value() ) );
                    Rhs.Reset();
                }
            } else {
                if( HasValue() ) {
                    Rhs.Construct( std::move( Value() ) );
                    Reset();
                } else {
                    // do nothing
                }
            }
        }
    };

    template< typename T, typename U >
    bool operator==( Optional< T > const& Lhs, Optional< U > const& Rhs ) noexcept( noexcept( std::declval< T const& >() == std::declval< U const& >() ) )
    {
        if( Lhs.HasValue() ^ Rhs.HasValue() )
            return false;
        if( !Lhs.HasValue() )
            return true;
        else
            return Lhs.Value() == Rhs.Value();
    }
    // other comparison operators omitted for brevity's sake

    template< typename T >
    void swap( Optional< T >& Lhs, Optional< T >& Rhs ) noexcept( noexcept( Lhs.Swap( Rhs ) ) )
    {
        Lhs.Swap( Rhs );
    }

    template< typename T >
    Optional< std::decay_t< T > > MakeOptional( T&& Arg ) noexcept( noexcept( Optional< std::decay_t< T > >{ std::forward< T >( Arg ) } ) )
    {
        return { std::forward< T >( Arg ) };
    }
    template< typename T, typename... ArgsT >
    Optional< T > MakeOptional( ArgsT&&... Args )
    {
        return { InPlace, std::forward< ArgsT >( Args )... };
    }
}

namespace std {
    template< typename T >
    struct hash< Symmap::Optional< T > > {
        using argument_type = Symmap::Optional< T >;
        using result_type = std::size_t;

        result_type operator()( argument_type const& Object ) const
        {
            if( !Object ) {
                return -1;
            } else {
                std::hash< T > Hasher;
                return Hasher( *Object );
            }
        }
    };
}
\$\endgroup\$
2
  • \$\begingroup\$ This code style (with the lack of new lines between logically separate declarations) is ... quite overwhelming. Is this best practice in C++? \$\endgroup\$
    – Alexander
    Sep 18, 2017 at 21:20
  • \$\begingroup\$ No map or flatmap? \$\endgroup\$
    – Alexander
    Sep 18, 2017 at 21:20

1 Answer 1

5
\$\begingroup\$

Why reinvent the wheel? If your motivation is merely "to support pre-C++17 compiler", then you'll be glad to know that:

Note, though, that there's this corner case of undefined behavior that wasn't resolved until C++17, and apparently you can't get it "officially" right before that. In practice it doesn't matter.

As for selection between implementations, here's what I do in my projects, which usually have a bunch of "utility" code under the util namespace; I expose the appropriate implementation as util::optional.

#if __cplusplus >= 201701L

#include <optional>

namespace util {
template <typename T>
using optional = std::optional<T>;
using nullopt = std::nullopt;
} // namespace util

#else
#if __cplusplus >= 201402L

#include <experimental/optional>

namespace util {
template <typename T>
using optional = std::experimental::optional<T>;
using nullopt_t = std::experimental::nullopt_t;
constexpr auto nullopt = std::experimental::nullopt;
} // namespace util

#else // pre-C++14 - must be C++11 or earlier

#error "C++11 and earlier not supported - you'll need to get your own implementation of std::optional (e.g. Boost's or https://github.com/akrzemi1/Optional)"

#endif // __cplusplus >= 201402L
#endif // __cplusplus >= 201701L

this doesn't support C++11 but you could replace the #error with inclusion of the Andrei's optional. If you don't like the util prefix - just do the same with any namespace you want, or even in the default namespace (although that's not a good idea for, say, a library).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.