6
\$\begingroup\$

I took quite some time to implement a fully standard-conforming std::optional in C++17. It turns out more sophisticated than I initially thought. My code is just below 1000 lines (excluding empty lines), and I have tested the functions extensively.

There have been some attempts to implement std::optional on Code Review. A simple search brings up two:

Some facts that complicate the implementation:

  • Many operations are constexpr friendly. With constexpr, the aligned_storage + explicit construction / destruction technique becomes useless. The standard is effectively asking us to use a union. The fact that the constexpr-ness on the copy / move operations depends on the triviality of the corresponding operations on the value type is a clear evidence because that's exactly how unions work.

  • The special member functions conditionally get defined as deleted / participate in overload resolution. Since special member functions cannot be templates, SFINAE cannot be used, and the only way to implement this that I can think of is to write a chain of base classes and use class template specialization, and then use = default to "inherit" the (possibly deleted) special member functions.

I used N4659 (C++17 final draft) as a reference. The relevant parts are [optional], [unord.hash], and [depr.func.adaptor.binding] (for the deprecated std::hash<...>::result_type and std::hash<...>::argument_type).

Except for std::hash, all functionalities are provided in the my_std namespace. As you can see, basically everything is boilerplate code and the actual code is almost zero.

// C++17 std::optional implementation

#ifndef INC_OPTIONAL_HPP_9AEkHPjv56
#define INC_OPTIONAL_HPP_9AEkHPjv56

#include <cassert>
#include <exception>
#include <initializer_list>
#include <memory>    // for std::destroy_at
#include <typeindex> // for std::hash
#include <typeinfo>
#include <type_traits>
#include <utility>

namespace my_std {

    // [optional.optional], class template optional
    template <class T>
    class optional;

    // [utility.syn], [in-place construction]
    struct in_place_t {
        explicit in_place_t() = default;
    };
    inline constexpr in_place_t in_place{};

    // [optional.nullopt], no-value state indicator
    struct nullopt_t {
        constexpr explicit nullopt_t(int) {}
    };
    inline constexpr nullopt_t nullopt{0};

    // [optional.bad.access], class bad_optional_access
    class bad_optional_access :public std::exception {
    public:
        bad_optional_access() = default;
    };

    // [optional.relops], relational operators
    template <class T, class U>
    constexpr bool operator==(const optional<T>&, const optional<U>&);
    template <class T, class U>
    constexpr bool operator!=(const optional<T>&, const optional<U>&);
    template <class T, class U>
    constexpr bool operator<(const optional<T>&, const optional<U>&);
    template <class T, class U>
    constexpr bool operator>(const optional<T>&, const optional<U>&);
    template <class T, class U>
    constexpr bool operator<=(const optional<T>&, const optional<U>&);
    template <class T, class U>
    constexpr bool operator>=(const optional<T>&, const optional<U>&);

    // [optional.nullops], comparison with nullopt
    template <class T>
    constexpr bool operator==(const optional<T>&, nullopt_t) noexcept;
    template <class T>
    constexpr bool operator==(nullopt_t, const optional<T>&) noexcept;
    template <class T>
    constexpr bool operator!=(const optional<T>&, nullopt_t) noexcept;
    template <class T>
    constexpr bool operator!=(nullopt_t, const optional<T>&) noexcept;
    template <class T>
    constexpr bool operator<(const optional<T>&, nullopt_t) noexcept;
    template <class T>
    constexpr bool operator<(nullopt_t, const optional<T>&) noexcept;
    template <class T>
    constexpr bool operator>(const optional<T>&, nullopt_t) noexcept;
    template <class T>
    constexpr bool operator>(nullopt_t, const optional<T>&) noexcept;
    template <class T>
    constexpr bool operator<=(const optional<T>&, nullopt_t) noexcept;
    template <class T>
    constexpr bool operator<=(nullopt_t, const optional<T>&) noexcept;
    template <class T>
    constexpr bool operator>=(const optional<T>&, nullopt_t) noexcept;
    template <class T>
    constexpr bool operator>=(nullopt_t, const optional<T>&) noexcept;

    // [optional.comp.with.t], comparison with T
    template <class T, class U>
    constexpr bool operator==(const optional<T>&, const U&);
    template <class T, class U>
    constexpr bool operator==(const U&, const optional<T>&);
    template <class T, class U>
    constexpr bool operator!=(const optional<T>&, const U&);
    template <class T, class U>
    constexpr bool operator!=(const U&, const optional<T>&);
    template <class T, class U>
    constexpr bool operator<(const optional<T>&, const U&);
    template <class T, class U>
    constexpr bool operator<(const U&, const optional<T>&);
    template <class T, class U>
    constexpr bool operator>(const optional<T>&, const U&);
    template <class T, class U>
    constexpr bool operator>(const U&, const optional<T>&);
    template <class T, class U>
    constexpr bool operator<=(const optional<T>&, const U&);
    template <class T, class U>
    constexpr bool operator<=(const U&, const optional<T>&);
    template <class T, class U>
    constexpr bool operator>=(const optional<T>&, const U&);
    template <class T, class U>
    constexpr bool operator>=(const U&, const optional<T>&);

    // [optional.specalg], specialized algorithms
    template <class T>
    std::enable_if_t<std::is_move_constructible_v<T> && std::is_swappable_v<T>>
        swap(optional<T>& x, optional<T>& y) noexcept(noexcept(x.swap(y)))
    {
        x.swap(y);
    }

    template <class T>
    constexpr optional<std::decay_t<T>> make_optional(T&& v)
    {
        return optional<std::decay_t<T>>(std::forward<T>(v));
    }
    template <class T, class... Args>
    constexpr optional<T> make_optional(Args&&... args)
    {
        return optional<T>(in_place, std::forward<Args>(args)...);
    }
    template <class T, class U, class... Args>
    constexpr optional<T> make_optional(std::initializer_list<U> il, Args&&... args)
    {
        return optional<T>(in_place, il, std::forward<Args>(args)...);
    }

}

namespace std {

    // [optional.hash], hash support
    template <class T>
    struct hash<my_std::optional<T>>;

}

namespace my_std::detail {
    template <class T, class U>
    struct is_cv_same :std::is_same<
        std::remove_const_t<std::remove_volatile_t<T>>,
        std::remove_const_t<std::remove_volatile_t<U>>
        > { };
    template <class T, class U>
    inline constexpr bool is_cv_same_v = is_cv_same<T, U>::value;

    template <class T>
    struct enable {
        // constructors
        template <class... Args>
        using in_place = std::enable_if_t<std::is_constructible_v<T, Args...>, int>;
        template <class U>
        using conv_implicit =
            std::enable_if_t<std::is_constructible_v<T, U&&> &&
                             !std::is_same_v<std::decay_t<U>, in_place_t> &&
                             !std::is_same_v<std::decay_t<U>, optional<T>> &&
                             std::is_convertible_v<U&&, T>, int>;
        template <class U>
        using conv_explicit =
            std::enable_if_t<std::is_constructible_v<T, U&&> &&
                             !std::is_same_v<std::decay_t<U>, in_place_t> &&
                             !std::is_same_v<std::decay_t<U>, optional<T>> &&
                             !std::is_convertible_v<U&&, T>, int>;
        template <class U>
        static constexpr bool conv_common =
            !std::is_constructible_v<T,       optional<U>& > &&
            !std::is_constructible_v<T,       optional<U>&&> &&
            !std::is_constructible_v<T, const optional<U>& > &&
            !std::is_constructible_v<T, const optional<U>&&> &&
            !std::is_convertible_v<      optional<U>& , T> &&
            !std::is_convertible_v<      optional<U>&&, T> &&
            !std::is_convertible_v<const optional<U>& , T> &&
            !std::is_convertible_v<const optional<U>&&, T>;
        template <class U>
        using copy_conv_implicit =
            std::enable_if_t<conv_common<U> &&
                             std::is_constructible_v<T, const U&> &&
                             std::is_convertible_v<const U&, T>, int>;
        template <class U>
        using copy_conv_explicit =
            std::enable_if_t<conv_common<U> &&
                             std::is_constructible_v<T, const U&> &&
                             !std::is_convertible_v<const U&, T>, int>;
        template <class U>
        using move_conv_implicit =
            std::enable_if_t<conv_common<U> &&
                             std::is_constructible_v<T, U&&> &&
                             std::is_convertible_v<U&&, T>, int>;
        template <class U>
        using move_conv_explicit =
            std::enable_if_t<conv_common<U> &&
                             std::is_constructible_v<T, U&&> &&
                             !std::is_convertible_v<U&&, T>, int>;

        // assignment
        template <class U>
        using conv_ass =
            std::enable_if_t<!std::is_same_v<optional<T>, std::decay_t<U>> &&
                             !(std::is_scalar_v<T> &&
                               std::is_same_v<T, std::decay_t<U>>) &&
                             std::is_constructible_v<T, U> &&
                             std::is_assignable_v<T&, U>, int>;
        template <class U>
        static constexpr bool conv_ass_common = conv_common<U> &&
            !std::is_assignable_v<T&,       optional<U>& > &&
            !std::is_assignable_v<T&, const optional<U>& > &&
            !std::is_assignable_v<T&,       optional<U>&&> &&
            !std::is_assignable_v<T&, const optional<U>&&>;
        template <class U>
        using copy_conv_ass =
            std::enable_if_t<conv_ass_common<U> &&
                             std::is_constructible_v<T, const U&> &&
                             std::is_assignable_v<T&, const U&>, int>;
        template <class U>
        using move_conv_ass =
            std::enable_if_t<conv_ass_common<U> &&
                             std::is_constructible_v<T, U> &&
                             std::is_assignable_v<T&, U>, int>;

        // emplace
        template <class U, class... Args>
        using emplace_ilist =
            std::enable_if_t<
                std::is_constructible_v<T, std::initializer_list<U>, Args...>
                , int>;
    };

    // deal with destructor
    // trivially destructible version
    template <class T, bool = std::is_trivially_destructible_v<T>>
    class destroy_base {
        static_assert(std::is_object_v<T>, "[optional.optional]/3");
        static_assert(std::is_destructible_v<T>, "[optional.optional]/3");
        static_assert(!detail::is_cv_same_v<T, in_place_t>, "[optional.syn]/1");
        static_assert(!detail::is_cv_same_v<T, nullopt_t>,  "[optional.syn]/1");
    public:
        constexpr destroy_base() noexcept {}
        ~destroy_base() = default;
        constexpr destroy_base(const destroy_base& rhs) = default;
        constexpr destroy_base(destroy_base&& rhs) = default;
        destroy_base& operator=(const destroy_base& rhs) = default;
        destroy_base& operator=(destroy_base&& rhs) = default;

        constexpr destroy_base(nullopt_t) noexcept {}
        template <class... Args,
                  typename enable<T>::template in_place<Args...> = 0>
        constexpr explicit destroy_base(in_place_t, Args&&... args)
            :object(std::forward<Args>(args)...), contains{true}
        {
        }
        template <class U, class... Args,
                  typename enable<T>::template in_place<std::initializer_list<U>&,
                                                        Args...> = 0>
        constexpr explicit destroy_base(in_place_t, std::initializer_list<U> ilist,
                                        Args&&... args)
            :object(ilist, std::forward<Args>(args)...), contains{true}
        {
        }

        constexpr bool has_value() const noexcept
        {
            return contains;
        }
        void reset() noexcept
        {
            destroy();
        }

    protected:
        constexpr T* get() noexcept
        {
            return &object;
        }
        constexpr const T* get() const noexcept
        {
            return &object;
        }
        template <typename... Args>
        void construct(Args&&... args)
        {
            assert(!has_value());
            ::new (get()) T(std::forward<Args>(args)...);
            contains = true;
        }
        void destroy() noexcept
        {
            assert(has_value());
            contains = false;
        }

    private:
        union {
            char dummy{'\0'};
            T object;
        };
        bool contains{false};
    };

    // non-trivially destructible version
    template <class T>
    class destroy_base<T, false> {
        static_assert(std::is_object_v<T>, "[optional.optional]/3");
        static_assert(std::is_destructible_v<T>, "[optional.optional]/3");
        static_assert(!detail::is_cv_same_v<T, in_place_t>, "[optional.syn]/1");
        static_assert(!detail::is_cv_same_v<T, nullopt_t>,  "[optional.syn]/1");
    public:
        constexpr destroy_base() noexcept {}
        constexpr destroy_base(const destroy_base& rhs) = default;
        constexpr destroy_base(destroy_base&& rhs) = default;
        destroy_base& operator=(const destroy_base& rhs) = default;
        destroy_base& operator=(destroy_base&& rhs) = default;

        ~destroy_base()
        {
            reset();
        }

        constexpr destroy_base(nullopt_t) noexcept {}
        template <class... Args,
                  typename enable<T>::template in_place<Args...> = 0>
        constexpr explicit destroy_base(in_place_t, Args&&... args)
            :object(std::forward<Args>(args)...), contains{true}
        {
        }
        template <class U, class... Args,
                  typename enable<T>::template in_place<std::initializer_list<U>&,
                                                        Args...> = 0>
        constexpr explicit destroy_base(in_place_t, std::initializer_list<U> ilist,
                                        Args&&... args)
            :object(ilist, std::forward<Args>(args)...), contains{true}
        {
        }

        constexpr bool has_value() const noexcept
        {
            return contains;
        }
        void reset() noexcept
        {
            if (has_value())
                destroy();
        }

    protected:
        constexpr T* get() noexcept
        {
            return &object;
        }
        constexpr const T* get() const noexcept
        {
            return &object;
        }
        template <typename... Args>
        void construct(Args&&... args)
        {
            assert(!has_value());
            ::new (get()) T(std::forward<Args>(args)...);
            contains = true;
        }
        void destroy() noexcept
        {
            assert(has_value());
            std::destroy_at(get());
            contains = false;
        }

    private:
        union {
            char dummy{'\0'};
            T object;
        };
        bool contains{false};        
    };

    template <class T>
    class common_base :public destroy_base<T> {
    public:
        using destroy_base<T>::destroy_base;

        constexpr common_base() = default;
        constexpr common_base(const common_base&) = default;
        constexpr common_base(common_base&&) = default;
        common_base& operator=(const common_base&) = default;
        common_base& operator=(common_base&&) = default;

        constexpr T* operator->()
        {
            assert(*this);
            return this->get();
        }
        constexpr const T* operator->() const
        {
            assert(*this);
            return this->get();
        }

        constexpr T& operator*() &
        {
            assert(*this);
            return *this->get();
        }
        constexpr const T& operator*() const &
        {
            assert(*this);
            return *this->get();
        }
        constexpr T&& operator*() &&
        {
            return std::move(*this->get());
        }
        constexpr const T&& operator*() const &&
        {
            return std::move(*this->get());
        }

        constexpr explicit operator bool() const noexcept
        {
            return this->has_value();
        }

    protected:
        // assign if has value, construct otherwise
        template <typename U>
        void assign(U&& arg)
        {
            if (this->has_value())
                **this = std::forward<U>(arg);
            else
                this->construct(std::forward<U>(arg));
        }
    };

    // deal with copy constructor
    // trivially copy constructible version
    template <class T, bool = std::is_copy_constructible_v<T>,
              bool = std::is_trivially_copy_constructible_v<T>>
    class copy_construct_base :public common_base<T> {
        using Base = common_base<T>;
    public:
        using Base::Base;

        constexpr copy_construct_base() = default;
        constexpr copy_construct_base(const copy_construct_base& rhs) = default;
        constexpr copy_construct_base(copy_construct_base&&) = default;
        copy_construct_base& operator=(const copy_construct_base&) = default;
        copy_construct_base& operator=(copy_construct_base&&) = default;
    };

    // non-trivially copy constructible version
    template <class T>
    class copy_construct_base<T, true, false> :public common_base<T> {
    public:
        using common_base<T>::common_base;

        constexpr copy_construct_base() = default;
        copy_construct_base(const copy_construct_base& rhs) // not constexpr
        {
            if (rhs)
                this->construct(*rhs);
        }
        constexpr copy_construct_base(copy_construct_base&&) = default;
        copy_construct_base& operator=(const copy_construct_base&) = default;
        copy_construct_base& operator=(copy_construct_base&&) = default;
    };

    // non-copy constructible version
    template <class T>
    class copy_construct_base<T, false, false> :public common_base<T> {
    public:
        using common_base<T>::common_base;

        constexpr copy_construct_base() = default;
        copy_construct_base(const copy_construct_base&) = delete;
        constexpr copy_construct_base(copy_construct_base&&) = default;
        copy_construct_base& operator=(const copy_construct_base&) = default;
        copy_construct_base& operator=(copy_construct_base&&) = default;
    };

    // deal with move constructor
    // trivially move constructible version
    template <class T, bool = std::is_move_constructible_v<T>,
              bool = std::is_trivially_move_constructible_v<T>>
    class move_construct_base :public copy_construct_base<T> {
        using Base = copy_construct_base<T>;
    public:
        using Base::Base;

        constexpr move_construct_base() = default;
        constexpr move_construct_base(const move_construct_base&) = default;
        constexpr move_construct_base(move_construct_base&& rhs)
            noexcept(std::is_nothrow_move_constructible_v<T>) = default;
        move_construct_base& operator=(const move_construct_base&) = default;
        move_construct_base& operator=(move_construct_base&&) = default;
    };

    // non-trivially move constructible version
    template <class T>
    class move_construct_base<T, true, false> :public copy_construct_base<T> {
    public:
        using copy_construct_base<T>::copy_construct_base;

        constexpr move_construct_base() = default; 
        constexpr move_construct_base(const move_construct_base&) = default;
        move_construct_base(move_construct_base&& rhs) // not constexpr
            noexcept(std::is_nothrow_move_constructible_v<T>)
        {
            if (rhs)
                this->construct(std::move(*rhs));
        }
        move_construct_base& operator=(const move_construct_base&) = default;
        move_construct_base& operator=(move_construct_base&&) = default;
    };

    // non-move constructible version
    template <class T>
    class move_construct_base<T, false, false> :public copy_construct_base<T> {
    public:
        using copy_construct_base<T>::copy_construct_base;

        constexpr move_construct_base() = default; 
        constexpr move_construct_base(const move_construct_base&) = default;
        move_construct_base(move_construct_base&& rhs) = delete;
        move_construct_base& operator=(const move_construct_base&) = default;
        move_construct_base& operator=(move_construct_base&&) = default;
    };

    // deal with copy assignment
    // copy constructible and assignable version
    template <class T, bool = (std::is_copy_constructible_v<T> &&
                               std::is_copy_assignable_v<T>)>
    class copy_assign_base :public move_construct_base<T> {
        using Base = move_construct_base<T>;
    public:
        using Base::Base;

        constexpr copy_assign_base() = default;
        constexpr copy_assign_base(const copy_assign_base&) = default;
        constexpr copy_assign_base(copy_assign_base&&) = default;
        copy_assign_base& operator=(const copy_assign_base& rhs)
        {
            if (rhs)
                this->assign(*rhs);
            else
                this->reset();
            return *this;
        }
        copy_assign_base& operator=(copy_assign_base&&) = default;
    };

    // non-(copy constructible and assignable) version
    template <class T>
    class copy_assign_base<T, false> :public move_construct_base<T> {
    public:
        using move_construct_base<T>::move_construct_base;

        constexpr copy_assign_base() = default;
        constexpr copy_assign_base(const copy_assign_base&) = default;
        constexpr copy_assign_base(copy_assign_base&&) = default;
        copy_assign_base& operator=(const copy_assign_base&) = delete;
        copy_assign_base& operator=(copy_assign_base&&) = default;
    };

    // deal with move assignment
    // move constructible and assignable version
    template <class T, bool = (std::is_move_constructible_v<T> &&
                               std::is_move_assignable_v<T>)>
    class move_assign_base :public copy_assign_base<T> {
        using Base = copy_assign_base<T>;
    public:
        using Base::Base;

        constexpr move_assign_base() = default;
        constexpr move_assign_base(const move_assign_base&) = default;
        constexpr move_assign_base(move_assign_base&&) = default;
        move_assign_base& operator=(const move_assign_base&) = default;
        move_assign_base& operator=(move_assign_base&& rhs)
            noexcept(std::is_nothrow_move_assignable_v<T> &&
                     std::is_nothrow_move_constructible_v<T>)
        {
            if (rhs)
                this->assign(std::move(*rhs));
            else
                this->reset();
            return *this;
        }
    };

    // non-(move constructible and assignable) version
    template <class T>
    class move_assign_base<T, false> :public copy_assign_base<T> {
    public:
        using copy_assign_base<T>::copy_assign_base;

        constexpr move_assign_base() = default;
        constexpr move_assign_base(const move_assign_base&) = default;
        constexpr move_assign_base(move_assign_base&&) = default;
        move_assign_base& operator=(const move_assign_base&) = default;
        move_assign_base& operator=(move_assign_base&&) = delete;
    };
}

namespace my_std {

    template <class T>
    class optional :public detail::move_assign_base<T> {
        using Base = detail::move_assign_base<T>;
        using Enable = detail::enable<T>;

    public:
        using value_type = T;

        using Base::Base;

        optional() = default;
        ~optional() = default;
        optional(const optional&) = default;
        optional(optional&&) = default;
        optional& operator=(const optional&) = default;
        optional& operator=(optional&&) = default;

        template <class U = T,
                  typename Enable::template conv_implicit<U> = 0>
        constexpr optional(U&& v)
            :Base{in_place, std::forward<U>(v)}
        {
        }
        template <class U = T,
                  typename Enable::template conv_explicit<U> = 0>
        explicit constexpr optional(U&& v)
            :Base{in_place, std::forward<U>(v)}
        {
        }
        template <class U,
                  typename Enable::template copy_conv_implicit<U> = 0>
        optional(const optional<U>& rhs)
        {
            if (rhs)
                this->construct(*rhs);
        }
        template <class U,
                  typename Enable::template copy_conv_explicit<U> = 0>
        explicit optional(const optional<U>& rhs)
        {
            if (rhs)
                this->construct(*rhs);
        }
        template <class U,
                  typename Enable::template move_conv_implicit<U> = 0>
        optional(optional<U>&& rhs)
        {
            if (rhs)
                this->construct(std::move(*rhs));
        }
        template <class U,
                  typename Enable::template move_conv_explicit<U> = 0>
        explicit optional(optional<U>&& rhs)
        {
            if (rhs)
                this->construct(std::move(*rhs));
        }

        optional& operator=(nullopt_t) noexcept
        {
            this->reset();
            return *this;
        }
        template <class U = T,
                  typename Enable::template conv_ass<U> = 0>
        optional& operator=(U&& v)
        {
            this->assign(std::forward<U>(v));
            return *this;
        }
        template <class U,
                  typename Enable::template copy_conv_ass<U> = 0>
        optional& operator=(const optional<U>& rhs)
        {
            if (rhs)
                this->assign(*rhs);
            else
                this->reset();
            return *this;
        }
        template <class U,
                  typename Enable::template move_conv_ass<U> = 0>
        optional& operator=(optional<U>&& rhs)
        {
            if (rhs)
                this->assign(std::move(*rhs));
            else
                this->reset();
            return *this;
        }

        template <class... Args>
        T& emplace(Args&&... args)
        {
            static_assert(std::is_constructible_v<T, Args...>,
                          "[optional.assign]/25");
            this->reset();
            this->construct(std::forward<Args>(args)...);
            return **this;
        }
        template <class U, class... Args,
                  typename Enable::template emplace_ilist<U, Args...> = 0>
        T& emplace(std::initializer_list<U> ilist, Args&&... args)
        {
            this->reset();
            this->construct(ilist, std::forward<Args>(args)...);
            return **this;
        }

        void swap(optional& rhs)
            noexcept(std::is_nothrow_move_constructible_v<T> &&
                     std::is_nothrow_swappable_v<T>)
        {
            if (*this && rhs) {
                using std::swap;
                swap(**this, *rhs);
            } else if (*this) {
                rhs.construct(std::move(**this));
                this->destroy();
            } else if (rhs) {
                this->construct(std::move(*rhs));
                rhs.destroy();
            }
        }

        constexpr T& value() &
        {
            if (*this)
                return **this;
            else
                throw bad_optional_access{};
        }
        constexpr const T& value() const &
        {
            if (*this)
                return **this;
            else
                throw bad_optional_access{};
        }
        constexpr T&& value() &&
        {
            if (*this)
                return std::move(**this);
            else
                throw bad_optional_access{};
        }
        constexpr const T&& value() const &&
        {
            if (*this)
                return std::move(**this);
            else
                throw bad_optional_access{};
        }

        template <class U>
        constexpr T value_or(U&& v) const &
        {
            static_assert(std::is_copy_constructible_v<T>, "[optional.observe]/18");
            static_assert(std::is_convertible_v<U&&, T>, "[optional.observe]/18");

            if (*this)
                return **this;
            else
                return static_cast<T>(std::forward<U>(v));
        }
        template <class U>
        constexpr T value_or(U&& v) &&
        {
            static_assert(std::is_move_constructible_v<T>, "[optional.observe]/20");
            static_assert(std::is_convertible_v<U&&, T>, "[optional.observe]/20");

            if (*this)
                return std::move(**this);
            else
                return static_cast<T>(std::forward<U>(v));
        }
    };

    template <class T>
    optional(T) -> optional<T>;

    template <class T, class U>
    constexpr bool operator==(const optional<T>& x, const optional<U>& y)
    {
        if (x)
            return y && static_cast<bool>(*x == *y);
        else
            return !y;
    }
    template <class T, class U>
    constexpr bool operator!=(const optional<T>& x, const optional<U>& y)
    {
        if (x)
            return !y || static_cast<bool>(*x != *y);
        else
            return static_cast<bool>(y);
    }
    template <class T, class U>
    constexpr bool operator<(const optional<T>& x, const optional<U>& y)
    {
        if (x)
            return y && static_cast<bool>(*x < *y);
        else
            return static_cast<bool>(y);
    }
    template <class T, class U>
    constexpr bool operator>(const optional<T>& x, const optional<U>& y)
    {
        if (x)
            return !y || static_cast<bool>(*x > *y);
        else
            return false;
    }
    template <class T, class U>
    constexpr bool operator<=(const optional<T>& x, const optional<U>& y)
    {
        if (x)
            return y && static_cast<bool>(*x <= *y);
        else
            return true;
    }
    template <class T, class U>
    constexpr bool operator>=(const optional<T>& x, const optional<U>& y)
    {
        if (x)
            return !y || static_cast<bool>(*x >= *y);
        else
            return !y;
    }

    template <class T>
    constexpr bool operator==(const optional<T>& x, nullopt_t) noexcept
    {
        return !x;
    }
    template <class T>
    constexpr bool operator==(nullopt_t, const optional<T>& x) noexcept
    {
        return !x;
    }
    template <class T>
    constexpr bool operator!=(const optional<T>& x, nullopt_t) noexcept
    {
        return static_cast<bool>(x);
    }
    template <class T>
    constexpr bool operator!=(nullopt_t, const optional<T>& x) noexcept
    {
        return static_cast<bool>(x);
    }
    template <class T>
    constexpr bool operator<(const optional<T>&, nullopt_t) noexcept
    {
        return false;
    }
    template <class T>
    constexpr bool operator<(nullopt_t, const optional<T>& x) noexcept
    {
        return static_cast<bool>(x);
    }
    template <class T>
    constexpr bool operator<=(const optional<T>& x, nullopt_t) noexcept
    {
        return !x;
    }
    template <class T>
    constexpr bool operator<=(nullopt_t, const optional<T>&) noexcept
    {
        return true;
    }
    template <class T>
    constexpr bool operator>(const optional<T>& x, nullopt_t) noexcept
    {
        return static_cast<bool>(x);
    }
    template <class T>
    constexpr bool operator>(nullopt_t, const optional<T>&) noexcept
    {
        return false;
    }
    template <class T>
    constexpr bool operator>=(const optional<T>&, nullopt_t) noexcept
    {
        return true;
    }
    template <class T>
    constexpr bool operator>=(nullopt_t, const optional<T>& x) noexcept
    {
        return !x;
    }

    template <class T, class U>
    constexpr bool operator==(const optional<T>& x, const U& v)
    {
        if (x)
            return *x == v;
        else
            return false;
    }
    template <class T, class U>
    constexpr bool operator==(const U& v, const optional<T>& x)
    {
        if (x)
            return v == *x;
        else
            return false;
    }
    template <class T, class U>
    constexpr bool operator!=(const optional<T>& x, const U& v)
    {
        if (x)
            return *x != v;
        else
            return true;
    }
    template <class T, class U>
    constexpr bool operator!=(const U& v, const optional<T>& x)
    {
        if (x)
            return v != *x;
        else
            return true;
    }
    template <class T, class U>
    constexpr bool operator<(const optional<T>& x, const U& v)
    {
        if (x)
            return *x < v;
        else
            return true;
    }
    template <class T, class U>
    constexpr bool operator<(const U& v, const optional<T>& x)
    {
        if (x)
            return v < *x;
        else
            return false;
    }
    template <class T, class U>
    constexpr bool operator<=(const optional<T>& x, const U& v)
    {
        if (x)
            return *x <= v;
        else
            return true;
    }
    template <class T, class U>
    constexpr bool operator<=(const U& v, const optional<T>& x)
    {
        if (x)
            return v <= *x;
        else
            return false;
    }
    template <class T, class U>
    constexpr bool operator>(const optional<T>& x, const U& v)
    {
        if (x)
            return *x > v;
        else
            return false;
    }
    template <class T, class U>
    constexpr bool operator>(const U& v, const optional<T>& x)
    {
        if (x)
            return v > *x;
        else
            return true;
    }
    template <class T, class U>
    constexpr bool operator>=(const optional<T>& x, const U& v)
    {
        if (x)
            return *x >= v;
        else
            return false;
    }
    template <class T, class U>
    constexpr bool operator>=(const U& v, const optional<T>& x)
    {
        if (x)
            return v >= *x;
        else
            return true;
    }

}

namespace my_std::detail {

    template <typename T>
    struct hash_is_enabled
        :std::is_default_constructible<std::hash<std::remove_const_t<T>>> {};
    template <typename T>
    inline constexpr bool hash_is_enabled_v = hash_is_enabled<T>::value;

    template <typename T>
    struct optional_hash {
        using result_type [[deprecated]] = std::size_t;
        using argument_type [[deprecated]] = my_std::optional<T>;

        constexpr std::size_t operator()(const optional<T>& o)
        {
            if (o)
                return std::hash<std::remove_const_t<T>>{}(*o);
            else
                return typeid(T).hash_code();
        }
    };

    struct disabled_hash {
        disabled_hash() = delete;
        disabled_hash(const disabled_hash&) = delete;
        disabled_hash& operator=(const disabled_hash&) = delete;
        disabled_hash(disabled_hash&&) = delete;
        disabled_hash& operator=(disabled_hash&&) = delete;
    };

}

namespace std {
    template <typename T>
    struct hash<my_std::optional<T>>
        :std::conditional_t<my_std::detail::hash_is_enabled_v<T>,
                            my_std::detail::optional_hash<T>,
                            my_std::detail::disabled_hash> {};
}

#endif

Here's the test if you want to see. It's a bit unorganized, and not the most important part :)

#include <cassert>
#include <string>
#include <vector>
#include "optional.hpp"

using namespace my_std;

struct Disabled {
    Disabled() = delete;
    Disabled(const Disabled&) = delete;
    Disabled& operator=(const Disabled&) = delete;
    Disabled(Disabled&&) = delete;
    Disabled& operator=(Disabled&&) = delete;
    ~Disabled() = default;
};

struct Nontrivial_copy {
    Nontrivial_copy() = default;
    Nontrivial_copy(const Nontrivial_copy&) {}
    Nontrivial_copy& operator=(const Nontrivial_copy&) = delete;
};

template <bool Noexcept = true>
struct Moveonly {
    Moveonly() = default;
    Moveonly(const Moveonly&) = delete;
    Moveonly& operator=(const Moveonly&) = delete;
    Moveonly(Moveonly&&) noexcept(Noexcept) {}
    Moveonly& operator=(Moveonly&&) noexcept(Noexcept) {}
};

struct Direct_init {
    // strict pattern
    constexpr Direct_init(int&, int&&) {}

    // no braced init
    template <class U>
    Direct_init(std::initializer_list<U>) = delete;
};

int main()
{
    // ill formed instantiation
    {
        // optional<int&> a;
        // optional<const in_place_t> b;
        // optional<volatile nullopt_t> c;
    }
    // value_type
    {
        static_assert(std::is_same_v<optional<int>::value_type, int>);
    }
    // deduction guide
    {
        static_assert(std::is_same_v<optional<int>, decltype(optional{42})>);
        static_assert(std::is_same_v<optional<Moveonly<>>,
                      decltype(optional{Moveonly<>{}})>);
    }
    // default / nullopt constructor
    {
        constexpr optional<int> a{};
        constexpr optional<int> b = nullopt;
        static_assert(!a);
        static_assert(!b);
        constexpr optional<Disabled> c{};
        constexpr optional<Disabled> d = nullopt;
        static_assert(!c);
        static_assert(!d);

        static_assert(std::is_nothrow_constructible_v<optional<Disabled>>);
        static_assert(std::is_nothrow_constructible_v<optional<int>, nullopt_t>);
    }
    // trivial (constexpr) copy constructor
    {
        constexpr optional<int> a{};
        constexpr auto b = a;
        static_assert(!a && !b);
        constexpr optional c{42};
        constexpr auto d = c;
        static_assert(c == 42 && d == 42);
    }
    // non-trivial (non-constexpr) copy constructor
    {
        constexpr optional<Nontrivial_copy> a{};
        constexpr optional<Nontrivial_copy> b{in_place};
        /* constexpr */ auto c = a;
        /* constexpr */ auto d = b;
        assert(!c);
        assert(d);
    }
    // deleted copy constructor
    {
        static_assert(!std::is_copy_constructible_v<optional<Disabled>>);
        static_assert(!std::is_copy_constructible_v<optional<Moveonly<>>>);
    }
    // move constructor
    {
        optional<Moveonly<true>> a{};
        auto b = std::move(a);
        assert(!a);
        assert(!b);
        optional<Moveonly<false>> c{in_place};
        auto d = std::move(c);
        assert(c);
        assert(d);
    }
    // move constructor noexcept specification
    {
        static_assert(std::is_nothrow_move_constructible_v<Moveonly<true>>);
        static_assert(!std::is_nothrow_move_constructible_v<Moveonly<false>>);
    }
    // deleted move constructor
    {
        static_assert(!std::is_move_constructible_v<optional<Disabled>>);
    }
    // in place constructor
    {
        int x = 21;
        constexpr optional<Direct_init> a{in_place, x, 42};
        static_assert(a);
    }
    // in place initializer list constructor
    {
        optional<std::vector<int>> b{in_place, {30, 36, 39, 42, 45}};
        assert((b == std::vector<int>{30, 36, 39, 42, 45}));
    }
    // in place constructor explicit
    {
        static_assert(!std::is_convertible_v<in_place_t, optional<Direct_init>>);
    }
    // single value constructor
    {
        optional<std::vector<int>> a{5}; // => std::vector<int>(5)
        assert(a->size() == 5);          // not 1
        constexpr optional<double> b = 42;
        static_assert(b == 42.0);
    }
    // explicit
    {
        static_assert(std::is_convertible_v<const char*, optional<std::string>>);
        static_assert(!std::is_convertible_v<std::size_t,
                      optional<std::vector<int>>>);
    }
    // copying converting constructor
    {
        optional<int> a{5};
        optional<double> b = a;
        optional<std::vector<int>> v{a}; // => std::vector<int>(5)
        assert(b == 5);
        assert(v->size() == 5);          // not 1
        static_assert(std::is_convertible_v<const optional<int>&,
                                            optional<double>>);
        static_assert(!std::is_convertible_v<const optional<int>&,
                                             optional<std::vector<int>>>);
        optional<int> c{};
        optional<double> d = c;
        optional<std::vector<int>> w{c};
        assert(!d && !w);
    }
    // moving converting constructor
    {
        optional<int> a{5};
        optional<double> b = std::move(a);
        optional<std::vector<int>> v{std::move(a)};
        assert(a == 5 && b == 5 && v->size() == 5);
        static_assert(!std::is_convertible_v<optional<int>&&,
                                             optional<std::vector<int>>>);
    }
    // destructor
    {
        static_assert(std::is_trivially_destructible_v<optional<Disabled>>);
        static_assert(!std::is_trivially_destructible_v<optional<std::string>>);
    }
    // nullopt assignment
    {
        optional<std::vector<std::string>> a{in_place, 5, "foo"};
        auto b = a;
        a = nullopt;
        assert(!a && b);
    }
    // copy assignment
    {
        optional<std::string> a;
        optional<std::string> b{"foo"};
        optional<std::string> c{"bar"};
        a = b;
        assert(a == "foo");
        a = c;
        assert(a == "bar");
        static_assert(!std::is_copy_assignable_v<optional<Disabled>>);
        static_assert(!std::is_copy_assignable_v<optional<Moveonly<>>>);
    }
    // move assignment
    {
        static_assert(std::is_nothrow_move_assignable_v<optional<Moveonly<>>>);
        static_assert(!std::is_nothrow_move_assignable_v<
                      optional<Moveonly<false>>>);
        static_assert(!std::is_move_assignable_v<Disabled>);
        optional<std::string> a{"foo"};
        optional<std::string> b{"bar"};
        b = std::move(a);
        assert(a == "" && b == "foo");
    }
    // single value assignment
    {
        optional<std::string> a{"foo"};
        a = "bar";
        static_assert(std::is_assignable_v<optional<std::string>&, const char*>);
        static_assert(!std::is_assignable_v<optional<std::string>&, int>);
    }
    // converting copy assignment
    {
        optional<std::string> a{"foo"};
        optional<const char*> b{"bar"};
        a = b;
        assert(a == "bar");
        static_assert(!std::is_assignable_v<optional<std::string>&,
                                            optional<int>&>);
    }
    // converting move assignment
    {
        optional<std::string> a{"foo"};
        optional<const char*> b{"bar"};
        a = std::move(b);
        assert(a == "bar" && b);
        static_assert(!std::is_assignable_v<optional<std::string>&, optional<int>>);
    }
    // emplace
    {
        optional<std::string> a{"foo"};
        optional<std::string> b{"bar"};
        a.emplace(5, 'a');
        assert(a == "aaaaa");
        a.emplace({'a', 'b', 'c'});
        assert(a == "abc");
        a.emplace(std::move(*b));
        assert(a == "bar" && b == "");
    }
    // swap, general
    {
        static_assert(std::is_nothrow_swappable_v<optional<Moveonly<>>>);
        static_assert(!std::is_nothrow_swappable_v<optional<Moveonly<false>>>);
        static_assert(!std::is_swappable_v<optional<Disabled>>);
   }
    // swap, case one
    {
        optional<int> a{1}, b{2};
        a.swap(b);
        assert(a == 2 && b == 1);
        swap(a, b);
        assert(a == 1 && b == 2);
    }
    // swap, case two
    {
        optional<int> a{1}, b;
        a.swap(b);
        assert(!a && b == 1);
        swap(a, b);
        assert(a == 1 && !b);
    }
    // swap, case three
    {
        optional<int> a, b{2};
        a.swap(b);
        assert(a == 2 && !b);
        swap(a, b);
        assert(!a && b == 2);
    }
    // swap, case four
    {
        optional<int> a, b;
        a.swap(b);
        assert(!a && !b);
        swap(a, b);
        assert(!a && !b);
    }
    // observers
    {
        optional<std::string> a{"foo"};
        assert(a->size() == 3);
        assert(*a == "foo");
        assert(a);
        assert(a.has_value());
        assert(a.value() == "foo");
        assert(a.value_or("bar") == "foo");

        optional<std::string> b{*std::move(a)};
        assert(a == "");
        a = "foo";
        b = std::move(a).value();
        assert(a == "");
        a = "foo";
        b = std::move(a).value_or("bar");
        assert(a == "" && b == "foo");

        constexpr optional<std::pair<int, int>> c;
        static_assert(!c && !c.has_value());
        // static_assert(c.value().first == 5); // throws bad_optional_access
        static_assert(c.value_or(std::pair(21, 42)) == std::pair(21, 42));
    }
    // reset
    {
        optional<std::string> a{"foo"};
        a.reset();
        assert(!a);
        a.reset();
        assert(!a);
    }
    // nullopt features
    {
        static_assert(std::is_empty_v<nullopt_t>);
        static_assert(!std::is_default_constructible_v<nullopt_t>);
        static_assert(!std::is_aggregate_v<nullopt_t>);
    }
    // bad_optional_access
    {
        static_assert(std::is_default_constructible_v<bad_optional_access>);
        static_assert(std::is_base_of_v<std::exception, bad_optional_access> &&
                      std::is_convertible_v<bad_optional_access*, std::exception*>);
    }
    // comparison between optionals
    {
        constexpr optional<int> a{42}, b{21}, c;
        static_assert(a == a && !(a == b) && c == c && !(a == c) && !(c == a));
        static_assert(!(a != a) && a != b && !(c != c) && a != c && c != a);
        static_assert(!(a < a) && !(a < b) && !(c < c) && !(a < c) && c < a);
        static_assert(a <= a && !(a <= b) && c <= c && !(a <= c) && c <= a);
        static_assert(!(a > a) && a > b && !(c > c) && a > c && !(c > a));
        static_assert(a >= a && a >= b && c >= c && a >= c && !(c >= a));
    }
    // comparison with nullopt
    {
        constexpr optional<int> a{42};
        static_assert(!(a == nullopt || nullopt == a));
        static_assert(a != nullopt && nullopt != a);
        static_assert(!(a < nullopt) && nullopt < a);
        static_assert(!(a <= nullopt) && nullopt <= a);
        static_assert(a > nullopt && !(nullopt > a));
        static_assert(a >= nullopt && !(nullopt >= a));

        constexpr optional<int> b;
        static_assert(b == nullopt && nullopt == b);
        static_assert(!(b != nullopt || nullopt != b));
        static_assert(!(b < nullopt) && !(nullopt < b));
        static_assert(b <= nullopt && nullopt <= b);
        static_assert(!(b > nullopt) && !(nullopt > b));
        static_assert(b >= nullopt && nullopt >= b);
    }
    // comparison with T
    {
        constexpr optional<double> a{42.0};
        static_assert(a == 42 && 42 == a && !(a == 21) && !(21 == a));
        static_assert(!(a != 42) && !(42 != a) && a != 21 && 21 != a);
        static_assert(!(a < 42) && !(42 < a) && !(a < 21) && 21 < a);
        static_assert(a <= 42 && 42 <= a && !(a <= 21) && 21 <= a);
        static_assert(!(a > 42) && !(42 > a) && a > 21 && !(21 > a));
        static_assert(a >= 42 && 42 >= a && a >= 21 && !(21 >= a));

        constexpr optional<double> b;
        static_assert(!(b == 42) && !(42 == b));
        static_assert(b != 42 && 42 != b);
        static_assert(b < 42 && !(42 < b));
        static_assert(b <= 42 && !(42 <= b));
        static_assert(!(b > 42) && 42 > b);
        static_assert(!(b >= 42) && 42 >= b);
    }
    // make optional
    {
        constexpr int ans = 42;
        auto a = make_optional(ans);
        static_assert(std::is_same_v<decltype(a), optional<int>>);
        assert(a == 42);

        constexpr auto b = make_optional<std::pair<double, double>>(ans, ans);
        static_assert(b == std::pair(42.0, 42.0));

        auto c = make_optional<std::vector<int>>({39, 42});
        assert((c == std::vector<int>{39, 42}));
    }
    // hash
    {
        assert(std::hash<optional<double>>{}(42) == std::hash<double>{}(42));

        using disabled = std::hash<optional<std::vector<double>>>;
        static_assert(!std::is_default_constructible_v<disabled>);
    }
}
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

This looks pretty good. My comments are trivial nitpicking.

  • The constructor of struct in_place_t gains nothing from explicit (it can't be considered as a conversion if it has no arguments). Whilst explicit prevents users writing in_place_t x = {}, I certainly think that's a reasonable thing to want to do, and won't cause any surprising conversions.

  • The comment // [optional.comp.with.t], comparison with T probably should read "comparison with value" or similar, given that the other argument is a const U&.

  • It shouldn't be necessary to provide my_std::swap(): providing member swap should be sufficient to allow std::swap() to work.

  • Instead of writing out the return type again in make_optional, we can simply use a brace-expression: return {std::forward<T>(v)};. Sadly this won't work for the in_place overloads as that uses an explicit constructor.

  • I'm not a fan of else return false in this:

        if (x)
            return *x == v;
        else
            return false;
    

    I'd probably rewrite as return x && *x == v;; similarly for all these related comparisons.

  • I don't think there's a need for static_cast<bool> in the optional/optional comparisons, since the the arguments of logical operators are contextually converted to bool.

\$\endgroup\$
6
  • 1
    \$\begingroup\$ Making the default constructor of in_place_t non-explicit allows code like in_place_t ip = {}; which is ill-formed according to the standard. The "comparison with T" is how the standard calls these function templates (which doesn't make much sense to me either). Also, are you sure that std::swap calls the swap member function? Otherwise, great review! \$\endgroup\$
    – L. F.
    Aug 9, 2019 at 12:45
  • \$\begingroup\$ That's a good point about explicit that I hadn't appreciated before - thanks for educating me! As for swap, I read "The expected way to make a user-defined type swappable is to provide a non-member function swap in the same namespace as the type: see Swappable for details." However, when I removed my_std::swap() and adding using std::swap; into the functions, I got a run-time assertion failure which I didn't probe further; I don't understand why there's a difference. \$\endgroup\$ Aug 9, 2019 at 12:52
  • \$\begingroup\$ "... and adding using std::swap; into the functions" into which functions? \$\endgroup\$
    – L. F.
    Aug 9, 2019 at 12:57
  • \$\begingroup\$ Into the test functions that call swap() (as indicated by compilation errors). \$\endgroup\$ Aug 9, 2019 at 13:27
  • 1
    \$\begingroup\$ Actually, = {} will cause surprising conversions - it allows {} to match a function parameter of the tag type. \$\endgroup\$
    – L. F.
    Dec 21, 2019 at 9:59

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.