8
\$\begingroup\$

Rationale: I often have the requirement to own object instances, while preserving polymorphic behavior (i.e. own the object and hold it by pointer, or reference).

This is usually expressed as a pointer (most of the times, std::unique_ptr<T>), but when the pointer is stored in a std:: container, client syntax becomes tricky (with - for example - vector iterators being dereferenced to a pointer - instead of a reference).

To avoid this, I have implemented a polymorphic wrapper for a (base) class, that holds a pointer to the base class, and allows it's population using a specialization.

The wrapper allows implicit conversion to the held type (as a Base [const] reference) and implements fast, non-intrusive cloning, for the held type. This is based on copy construction of the Specialized type and stdex::details::polymorphic_clone<Base,Specialized> template specialization, to clone the correct Specialized type).

Please review the code, and let me know if there are any pitfalls or design/implementation problems with it (one known limitation is described at the end).

#pragma once

#include <memory>
#include <cassert>
#include <functional>
#include <stdexcept>
#include <vector>

namespace stdex {
    inline namespace details {

        /// @brief Deep copy construct from (Specialized&)*src
        ///
        /// @retval nullptr if src is nullptr
        /// @retval Specialized clone of *src
        ///
        /// @note Undefined behavior if src does not point to a Specialized*
        template<typename Base, typename Specialized>
        Base* polymorphic_clone (const Base* src) {
            static_assert(std::is_base_of<Base, Specialized>::value,
                "Specialized is not a specialization of Base");

            if (src == nullptr)
                return nullptr;
            return new Specialized{ static_cast<const Specialized&>(*src) };
        }
    }

    /// @brief polymorphic reference interface over a base class
    ///
    /// Respects polymorphic behavior of class ref.
    /// Instances have deep copy semantics (clone) and
    /// "[const] Base&" interface
    ///
    /// @note Not regular: no trivial way to implement non-intrusive equality
    ///
    /// @note safe to use with standard containers
    template<typename Base>
    class polymorphic final
    {
    public:

        /// Functor capable to convert a Base* to it's specialized type
        /// and clone it (intrusive implementation can be used)
        ///
        /// example intrusive implementation (if supported by Base):
        /// []( const Base* src ) { return src->clone(); }
        typedef std::function<Base* (const Base*)>  clone_functor;

        /// @brief construct (takes ownership of ptr)
        template<typename Specialized, typename CloneSpecialized>
        polymorphic(Specialized* ptr, CloneSpecialized functor) noexcept
        : instance_{ptr}, clone_{std::move(functor)}
        {
            static_assert(std::is_base_of<Base, Specialized>::value,
            "Specialized is not a specialization of Base");
            static_assert(
            std::is_constructible<clone_functor, CloneSpecialized>::value,
            "CloneSpecialized is not valid for a clone functor");
        }

        // not implemented: UB cloning in case client provides specialized ptr
        // polymorphic(Base* ptr);

        // @note empty constructor for std:: containers support
        polymorphic() = default;

        polymorphic(polymorphic&&) = default;

        polymorphic(const polymorphic& other)
        // : polymorphic{std::move(other.clone())}
        : polymorphic{ other.clone() } // comment by @dyp
        {
        }

        // polymorphic& operator=(polymorphic other)
        polymorphic& operator=(polymorphic other) noexcept // comment by @dyp
        {
            std::swap(instance_, other.instance_);
            std::swap(clone_, other.clone_);
            return *this;
        }

        ~polymorphic() = default;

        /// @brief Cast to contained type
        /// @pre instance not moved
        /// @pre *this initialized with valid instance
        operator Base&() const
        {
            assert(instance_.get());
            return *instance_.get();
        }

        /// @brief Cast to contained type
        /// @pre instance not moved
        /// @pre *this initialized with valid instance
        operator const Base&() const
        {
            assert(instance_.get());
            return *instance_.get();
        }

    private:
        polymorphic clone() const
        {
            return { clone_(instance_.get()), clone_functor{clone_} };
        }

        std::unique_ptr<Base>   instance_;
        clone_functor           clone_;
    };

    // edited after comment by @dyp
    template<typename Base, typename Specialized, typename CF>
    polymorphic<Base> to_polymorphic(Specialized&& temp, CF functor)
    {
        return {
            new Specialized{std::move(temp)},
            typename polymorphic<Base>::clone_functor{std::move(functor)}
        };
    }

    template<typename Base, typename Specialized>
    polymorphic<Base> to_polymorphic(Specialized&& temp)
    {
        static_assert(std::is_base_of<Base, Specialized>::value,
        "Specialized is not a specialization of Base");

        return to_polymorphic<Base,Specialized>(
            std::move(temp), polymorphic_clone<Base,Specialized>
        );
    }

    template<typename Base, typename Specialized, typename ...Args>
    // polymorphic<Base> to_polymorphic(Args ...args)
    polymorphic<Base> to_polymorphic(Args&& ...args) // comment by @dyp
    {
        static_assert(std::is_constructible<Specialized, Args...>::value,
        "Cannot instantiate Specialized from arguments");

        return to_polymorphic<Base,Specialized>(
            std::move(Specialized{std::forward<Args...>(args...)}));
    }

    template<typename Base> using polymorphic_vector =
    std::vector<polymorphic<Base>>;

    template<typename Base, typename ...Args>
    polymorphic_vector<Base> to_polymorphic_vector(Args&& ...args)
    {
        // comment by @dyp (add std::forward)
        return {
            to_polymorphic<Base>(std::forward<Args>(args))...
        };
    }

} // stdex

Example use (using a class hierarchy based on view, a generic responder for HTTP requests - the implementation of view is not important here, I just had it in existing code):

stdex::polymorphic_vector<view> views = // explicit type for clarity
    stdex::to_polymorphic_vector<view>(
        echo_view{"/echo"}, // class echo_view : public view
        directory_view{"/static_files", "~/http-server/static"}
            // class directory_view : public view
    );

for(auto& v: views)
    if(v.matches(reuqest.url())) // bool view::matches(...);
        auto response = v.handle(request); // virtual view::handle(...) = 0;

Limitations of this implementation:

If you use multiple inheritance DO NOT USE stdex::details::polymorphic_clone. Write an implementation based on dynamic_cast instead, and use to_polymorphic(Specialized&& temp, CF functor).

\$\endgroup\$
  • 1
    \$\begingroup\$ polymorphic{std::move(other.clone())} the move seems unnecessary here, and prevents move elision. \$\endgroup\$ – dyp Jun 18 '14 at 14:53
  • 1
    \$\begingroup\$ polymorphic<Base> to_polymorphic(Args ...args) Bug: missing &&. Similarly, in to_polymorphic<Base>(args), missing forward. \$\endgroup\$ – dyp Jun 18 '14 at 15:03
  • 1
    \$\begingroup\$ I don't quite understand the problem with multiple inheritance here. OTOH, putting an object via reference into polymorphic via to_derived requires the user to make sure it's of the most-derived type, otherwise the cloning will slice. \$\endgroup\$ – dyp Jun 18 '14 at 15:10
  • 1
    \$\begingroup\$ polymorphic{ std::other.clone() } <- the std:: should be left out. std::forward(args) should be std::forward<Args>(args) \$\endgroup\$ – dyp Jun 18 '14 at 17:47
  • 1
    \$\begingroup\$ @dyp You should really post a full answer when you have that many things to say. It would help everyone (and, IIRC, it is not the first time that you post enough comments in such a short time that could make a good answer) :) \$\endgroup\$ – Morwenn Jun 18 '14 at 19:30
3
+100
\$\begingroup\$

Sometimes you are not using universal references in the right way (i.e. use always std::forward when passing them around and not std::move). Fix it in the implementation of to_polymorphic, for example.

The assignment operator takes the other variable by value. Consider splitting it in a (default) move assignment operator noexcept that takes by rvalue-reference and a copy assignment operator that takes by const reference and performs a clone.

I don't see big problems in your implementation, but I recommend you to compare your approach with the Sean Parent's one (see also http://youtu.be/bIhUE5uUFOA), as already suggested by @dyp. One limitation, for example is that you are not using Small Buffer Optimization and you are forcing polymorphism trough inheritance.

There are other implementations providing value semantics along with polymorphism. For example, Adobe poly and Boost Type Erasure (see usage examples here).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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