I'm writing a simple wrapper for std::unique_ptr
, which copies the pointed object when copied.
Unlike this wrapper, it properly copies derived classes if
unique_ptr
points to a base class.
Also it supports a feature similar to std::visit
, which is described below.
(As noted by @Quuxplusone, deep-copying feature resembles that of std::any
.)
Here's a short description of my class.
(The implementation is at the bottom of the question.)
- It supports conventional
operator*
,operator->
,T* get()
, andoperator bool
. - It doesn't have
.reset()
, butmy_obj = {};
can be used instead. - Can be copy constructed/assigned. When copied, copies the underlying object.
- Can't aquire ownership of an external pointer, and can't release ownership of stored pointer.
std::make_unique
-esque construction:DynStorage<T> my_obj = DynStorage<T>::make(…);
.
Can be written asDynStorage<T> my_obj(…);
if argument list is not empty.
Example usage:auto x = DynStorage<int>::make(42); std::cout << x.get() << '\n'; // Prints 42
DynStorage<Derived>
can't be converted toDynStorage<Base>
(to simplify the implementation).
The only way to make aDynStorage<Base>
that points to a derived class is to useDynStorage<Base> my_obj = DynStorage<Base>::make<Derived>(…);
const DynStorage<T>
doesn't allow you to modify the pointed object (unlikeunique_ptr
).- Pointers to arrays aren't supported.
Supports a feature similar to
std::visit
, but you have to specify every possible visitor function in advance in an (optional) template parameter ofDynStorage
:// Assume we have some classes: struct A {virtual ~A() {}}; struct B : A {}; // Some overloaded (or template) function: void foo(A) {std::cout << "foo(A)\n";} void foo(B) {std::cout << "foo(B)\n";} // And we want to call a correct overload on a pointed object: int main() { auto x = DynStorage<A>::make(); // Makes an instance of A auto y = DynStorage<A>::make<B>(); // Makes an instance of B foo(*x); // Prints foo(A) foo(*y); // Prints foo(A), but we want foo(B) } // Here is how we do that: template <typename BaseT> struct MyFuncsBase : DynamicStorage::func_base<BaseT> { virtual void call_foo(BaseT *) = 0; using Base = MyFuncsBase; }; template <typename BaseT, typename DerivedT> struct MyFuncs : MyFuncsBase<BaseT> { void call_foo(BaseT *base) override { foo(*DynamicStorage::derived<DerivedT>(base)); } }; int main() { auto x = DynStorage<A,MyFuncs>::make(); auto y = DynStorage<A,MyFuncs>::make<B>(); x.functions().call_foo(x.get()); // prints foo(A) y.functions().call_foo(y.get()); // prints foo(B) }
The implementation
dynamic_storage.h
#ifndef DYN_STORAGE_H_INCLUDED
#define DYN_STORAGE_H_INCLUDED
#include <memory>
#include <type_traits>
#include <utility>
namespace DynamicStorage
{
namespace impl
{
// Type trait to check if A is static_cast'able to B.
template <typename A, typename B, typename = void> struct can_static_cast_impl
: std::false_type {};
template <typename A, typename B> struct can_static_cast_impl<A, B,
std::void_t<decltype(static_cast<B>(std::declval<A>()))>> : std::true_type {};
template <typename A, typename B> inline constexpr bool can_static_cast_v =
can_static_cast_impl<A,B>::value;
// Type trait to check if A is dynamic_cast'able to B.
template <typename A, typename B, typename = void> struct can_dynamic_cast_impl
: std::false_type {};
template <typename A, typename B> struct can_dynamic_cast_impl<A, B,
std::void_t<decltype(dynamic_cast<B>(std::declval<A>()))>> : std::true_type {};
template <typename A, typename B> inline constexpr bool can_dynamic_cast_v =
can_dynamic_cast_impl<A,B>::value;
template <typename A, typename B>
inline constexpr bool can_static_or_dynamic_cast_v =
can_static_cast_v<A,B> || can_dynamic_cast_v<A,B>;
template <typename T> T *get_instance()
{
static T ret;
return &ret;
}
}
// Downcasts a pointer. Attempts to use static_cast, falls back
// to dynamic_cast. If none of them work, fails with a static_assert.
template <typename Derived, typename Base> Derived *derived(Base *ptr)
{
static_assert(impl::can_static_or_dynamic_cast_v<Base*, Derived*>,
"Pointer to derived can't be obtained from pointer to base.");
if constexpr (impl::can_static_cast_v<Base*, Derived*>)
return static_cast<Derived *>(ptr); // This doesn't work if base is virtual.
else
return dynamic_cast<Derived *>(ptr);
}
template <typename B> struct func_base
{
using Base = func_base;
virtual std::unique_ptr<B> copy_(const B *) = 0;
};
template <typename B, typename D> struct default_func_impl : func_base<B> {};
template
<
typename T,
template <typename,typename> typename Functions = default_func_impl
>
class DynStorage
{
static_assert(!std::is_const_v<T>, "Template parameter can't be const.");
static_assert(!std::is_array_v<T>, "Template parameter can't be an array.");
template <typename D> struct Implementation : Functions<T,D>
{
static_assert(impl::can_static_or_dynamic_cast_v<T*, D*>,
"Pointer to derived can't be obtained from pointer to base.");
std::unique_ptr<T> copy_(const T *ptr) override
{
return ptr ? std::make_unique<D>(*derived<const D>(ptr))
: std::unique_ptr<T>();
}
};
using Pointer = std::unique_ptr<T>;
using FuncBase = typename Implementation<T>::Base;
FuncBase *funcs = impl::get_instance<Implementation<T>>();
Pointer ptr;
public:
// Makes a null pointer.
DynStorage() noexcept {}
// Constructs an object of type T from a parameter pack.
template <typename ...P, typename = std::void_t<decltype(T(std::declval<P>()...))>>
DynStorage(P &&... p) : ptr(std::make_unique<T>(std::forward<P>(p)...)) {}
DynStorage(const DynStorage &other)
: funcs(other.funcs), ptr(funcs->copy_(other.ptr.get())) {}
DynStorage(DynStorage &&other) noexcept
: funcs(other.funcs), ptr(std::move(other.ptr)) {}
DynStorage &operator=(const DynStorage &other)
{
ptr = other.funcs->copy_(other.ptr.get());
funcs = other.funcs;
return *this;
}
DynStorage &operator=(DynStorage &&other) noexcept
{
ptr = std::move(other.ptr);
funcs = other.funcs;
return *this;
}
// Constructs an object of type T (by default)
// or a derived type from a parameter pack.
template <typename D = T, typename ...P,
typename = std::void_t<decltype(D(std::declval<P>()...))>>
[[nodiscard]] static DynStorage make(P &&... p)
{
static_assert(!std::is_const_v<D>, "Template parameter can't be const.");
static_assert(!std::is_array_v<D>, "Template parameter can't be an array.");
static_assert(std::is_same_v<D,T> || std::has_virtual_destructor_v<T>,
"Base has to have a virtual destructor.");
DynStorage ret;
ret.ptr = std::make_unique<D>(std::forward<P>(p)...);
ret.funcs = impl::get_instance<Implementation<D>>();
return ret;
}
[[nodiscard]] explicit operator bool() const {return bool(ptr);}
[[nodiscard]] T *get() {return ptr.get();}
[[nodiscard]] const T *get() const {return ptr.get();}
[[nodiscard]] T &operator*() {return *ptr;}
[[nodiscard]] const T &operator*() const {return *ptr;}
[[nodiscard]] T *operator->() {return *ptr;}
[[nodiscard]] const T *operator->() const {return *ptr;}
FuncBase &functions() const {return *funcs;}
};
}
using DynamicStorage::DynStorage;
#endif
Some thoughts:
- It seems to work, but I'm not sure if I handle all possible exceptions correctly.
- I don't like the visiting syntax (especially visitor definitions), but I'm not sure how to improve it.
- It seems that copying (and visiting) could be optimized a bit by using function pointers instead of virtual functions, but I'm not sure how to do it elegantly.
unique_ptr
, but copyable, and can hold any derived type, and can be visited via RTTI" smells an awful lot likestd::any
. Consider recasting your question (and your intuition about the desired behaviors of your class) in terms of a "visitablestd::any
." \$\endgroup\$static_assert
s.) \$\endgroup\$std::any
. I decided to present it asunique_ptr
-like class because stored types are restriced to classes derived from a common base, and it's possible easily get a pointer to that common base (which makes it different fromstd::any
, and I want to keep this behaviour). I think I'll leave the question mostly unchanged, but add a note about it resemblingstd::any
. \$\endgroup\$