More C++ Idioms/Polymorphic Value Types

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Polymorphic Value Types

[edit | edit source]

Intent

[edit | edit source]

Support run-time polymorphism of value types without requiring inheritance relationship.

Also Known As

[edit | edit source]

Run-time Concept

Motivation

[edit | edit source]

Sometimes, one wants a type-erased container for types that implement some interface.

Solution and Sample Code

[edit | edit source]

By defining a wrapper class template that accepts types satisfying an interface and implements that interface itself by forwarding, but inherits from a base class that requires that derived classes implement that interface, then wrapping the wrapper class template and base class in another class, that class can exhibit polymorphic behavior without the stored values having an is-a relationship with an interface. The below class is a type-erased wrapper that stores any type that implements copying, equality comparison, and hashing. Its introduction into a codebase does not require any changes to types if they already implement the interface.

class copyable_hashable{
    private:
        template <class T>
        static constexpr bool is_in_place_type = false;
        template <class T>
        static constexpr bool is_in_place_type<std::in_place_type_t<T>> = true;

        struct impl;

        template <class T>
        struct impl_model;

        template <class T>
        using stored = impl_model<std::remove_cvref_t<T>>;

        std::unique_ptr<impl> data_{};
    public:
        template <class T>
        static constexpr bool storable = (
            std::copy_constructible<T>
            && std::equality_comparable<T>
            && requires (const T& val){
                std::invoke_r<std::size_t>(std::hash<T>(), val);
            }
        );

        constexpr copyable_hashable() noexcept = default;
        constexpr copyable_hashable(const copyable_hashable&);
        constexpr copyable_hashable(copyable_hashable&&) noexcept = default;
        template <class T, class... Args>
        requires (
            storable<std::remove_cvref_t<T>>
            && std::constructible_from<std::remove_cvref_t<T>, Args...>
        )
        constexpr copyable_hashable(std::in_place_type_t<T>, Args&&... args) :
            data_(std::make_unique<stored<T>>(std::forward<Args>(args)...)){}
        template <class T>
        requires (
            !std::is_same_v<std::remove_cvref_t<T>, copyable_hashable>
            && !is_in_place_type<std::remove_cvref_t<T>>
            && storable<std::remove_cvref_t<T>>
            && std::constructible_from<std::remove_cvref_t<T>, T>
        )
        constexpr copyable_hashable(T&& value) :
            copyable_hashable(std::in_place_type<T>, std::forward<T>(value)){}

        constexpr copyable_hashable& operator=(copyable_hashable rhs) noexcept{
            swap(rhs);
            return *this;
        }

        template <class T>
        requires (!std::is_reference_v<T>)
        constexpr T& get() & noexcept{
            return dynamic_cast<stored<T>&>(*data_).value;
        }
        template <class T>
        requires (!std::is_reference_v<T>)
        constexpr T const& get() const& noexcept{
            return dynamic_cast<stored<T> const&>(*data_).value;
        }
        template <class T>
        requires (!std::is_reference_v<T>)
        constexpr T&& get() && noexcept{
            return std::move(dynamic_cast<stored<T>&>(*data_).value);
        }
        template <class T>
        requires (!std::is_reference_v<T>)
        constexpr T const&& get() const&& noexcept{
            return std::move(dynamic_cast<stored<T> const&>(*data_).value);
        }
        
        template <class T>
        constexpr T* get_ptr() noexcept{
            auto const ptr = dynamic_cast<stored<T>*>(data_.get());
            return ptr ? std::addressof(ptr->value) : nullptr;
        }
        template <class T>
        constexpr T const* get_ptr() const noexcept{
            auto const ptr = dynamic_cast<stored<T> const*>(data_.get());
            return ptr ? std::addressof(ptr->value) : nullptr;
        }

        constexpr const std::type_info& type() const noexcept;

        constexpr void swap(copyable_hashable& other) noexcept{
            data_.swap(other.data_);
        }

        constexpr bool operator==(const copyable_hashable&) const;

        friend struct std::hash<copyable_hashable>;
};

struct copyable_hashable::impl{
    constexpr virtual ~impl() noexcept = default;
    constexpr virtual std::unique_ptr<impl> clone() const = 0;
    constexpr virtual const std::type_info& type() const noexcept = 0;
    constexpr virtual bool equals(const impl&) const = 0;
    constexpr virtual std::size_t hash() const = 0;
};

constexpr copyable_hashable::copyable_hashable(const copyable_hashable& other) :
    data_(other.data_ ? other.data_->clone() : nullptr){}

constexpr const std::type_info& copyable_hashable::type() const noexcept{
    return data_ ? data_->type() : typeid(void);
}

constexpr bool copyable_hashable::operator==(
    const copyable_hashable& other
) const{
    bool const neither_null = data_ && other.data_;
    return neither_null ? data_->equals(*other.data_) : data_ == other.data_;
}

template <class T>
struct copyable_hashable::impl_model : public impl{
    T value;

    template <class... Args>
    constexpr impl_model(Args&&... args) : value(std::forward<Args>(args)...){}

    constexpr std::unique_ptr<impl> clone() const override{
        return std::make_unique<impl_model>(value);
    }

    constexpr const std::type_info& type() const noexcept override{
        return typeid(T);
    }

    constexpr bool equals(const impl& other) const override{
        if (auto const other_ptr = dynamic_cast<impl_model const*>(&other))
            return value == other_ptr->value;
        return false;
    }

    constexpr std::size_t hash() const override{
        return std::invoke_r<std::size_t>(std::hash<T>(), value);
    }
};

constexpr void swap(copyable_hashable& lhs, copyable_hashable& rhs) noexcept{
    lhs.swap(rhs);
}

template <>
struct std::hash<copyable_hashable>{
    constexpr std::size_t operator()(const copyable_hashable& val) const{
        return val.data_ ? val.data_->hash() : 0uz;
    }
};

Known Uses

[edit | edit source]
[edit | edit source]

References

[edit | edit source]