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)
.
polymorphic{std::move(other.clone())}
themove
seems unnecessary here, and prevents move elision. \$\endgroup\$polymorphic<Base> to_polymorphic(Args ...args)
Bug: missing&&
. Similarly, into_polymorphic<Base>(args)
, missingforward
. \$\endgroup\$polymorphic
viato_derived
requires the user to make sure it's of the most-derived type, otherwise the cloning will slice. \$\endgroup\$polymorphic{ std::other.clone() }
<- thestd::
should be left out.std::forward(args)
should bestd::forward<Args>(args)
\$\endgroup\$