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.
1 Answer 1
That's really neat! I really like the idea of provoking the on-demand instantiation of the polymorphic dispatcher by passing it as a template template parameter.
That being said, I agree with you that the dispatching syntax can be improved quite a bit.
x.functions().call_foo(x.get()); // Yuck!
How about something that looks more like a proper visitor:
visit(x, &MyFuncsBase::call_foo);
This can work because pointers to member functions can be dispatched polymorphically. Here's a rough outline of how i'd go about pulling that syntax off:
#include <memory>
#include <iostream>
template <typename T, template<typename> typename DispatcherT>
class DynStorage {
using base_dispatcher = typename DispatcherT<T>::base_t;
struct StorageBase {
public:
virtual ~StorageBase() {}
virtual T* getData() = 0;
virtual base_dispatcher* getDispatcher() = 0;
};
template<typename U>
struct StorageImpl : public StorageBase {
U data_;
static DispatcherT<U> dispatcher_;
public:
base_dispatcher* getDispatcher() override {
return &dispatcher_;
}
T* getData() override {
return &data_;
}
};
std::unique_ptr<StorageBase> storage_;
public:
DynStorage(std::unique_ptr<StorageBase> s) : storage_(std::move(s)) {}
template<typename R, typename... argsT>
R dispatch(R(base_dispatcher::*func)(T const&, argsT...), argsT... args) const {
return (storage_->getDispatcher()->*func)(*storage_->getData(), args...);
}
template<typename U=T>
static DynStorage make() {return DynStorage(std::make_unique<StorageImpl<U>>()); }
};
template<typename T, template<typename> typename DispatcherT, typename CB_T, typename... argsT>
decltype(auto) visit(DynStorage<T, DispatcherT> const& x, CB_T cb, argsT... args ) {
return x.dispatch(cb, args...);
}
struct A {};
struct B : public A {};
struct MyFuncsBase {
// no need for a virtual destructor
virtual void foo(A const& val) = 0 ;
virtual int bar(A const& val, float v ) = 0;
};
template<typename T>
struct MyFuncs : public MyFuncsBase {
using base_t = MyFuncsBase;
void foo(A const& val) override {
std::cout << typeid(T).name() << std::endl;
}
int bar(A const& val, float v ) override {
return 0;
}
};
int main()
{
auto x = DynStorage<A, MyFuncs>::make();
auto y = DynStorage<A, MyFuncs>::make<B>();
visit(x, &MyFuncsBase::foo);
visit(y, &MyFuncsBase::foo);
// Ooooh, arguments and return type support too!
int res = visit(y, &MyFuncsBase::bar, 12.0f);
return 0;
}
Obviously, this needs some cleanup, some proper forwarding semantics, etc... It's just here to demonstrate how the better syntax can be implemented.
Edit:
I also really dislike that universal singleton:
template <typename T> T *get_instance()
{
static T ret;
return &ret;
}
Not only is it unnecessary, but it also brings a hefty amount of per-call overhead.
You are also coding way too defensively for my taste. all of these can_static_cast
and can_dynamic_cast
are all redundant with the checks that the compiler performs when assigning values to ptr
.
If you want to provide users with clearer errors when they mess up, then a simple std::is_base_of<>
will suffice.
similarly, the following is perfectly fine for the users to do:
void call_foo(BaseT *base) override
{
foo(static_cast<DerivedT*>(base));
}
So that derived<>()
member function seems like extra api surface for no reason.
-
\$\begingroup\$ About
get_instance
: Good point, I'll replace it with a static object inDynStorage<T>::Implementation
. About thederived<>()
: The intent was to make an universal downcasting function, sincestatic_cast
doesn't work with virtual bases. About the alternative visiting syntax: Looks interesting, but the downside is that it requires writingMyFuncsBase::
each time. I guess I need to think what do I like more. \$\endgroup\$HolyBlackCat– HolyBlackCat2018年07月01日 12:23:54 +00:00Commented Jul 1, 2018 at 12:23 -
\$\begingroup\$ You could make an argument that a universal downcaster is a potentially useful tool in general, but I see it as something orthogonal. I think you should just make a
down_cast<>
template function as a complete separate library/feature, and limitDynStorage
to only performing the work it's supposed to do. \$\endgroup\$user128454– user1284542018年07月01日 12:43:27 +00:00Commented Jul 1, 2018 at 12:43 -
\$\begingroup\$
func_base::copy_
requiresderived<>()
to function properly, but maybe I should put it into a separate header indeed. \$\endgroup\$HolyBlackCat– HolyBlackCat2018年07月01日 13:17:51 +00:00Commented Jul 1, 2018 at 13:17
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\$