3
\$\begingroup\$

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(), and operator bool.
  • It doesn't have .reset(), but my_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 as DynStorage<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 to DynStorage<Base> (to simplify the implementation).
    The only way to make a DynStorage<Base> that points to a derived class is to use DynStorage<Base> my_obj = DynStorage<Base>::make<Derived>(…);

  • const DynStorage<T> doesn't allow you to modify the pointed object (unlike unique_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 of DynStorage:

    // 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.
\$\endgroup\$
  • \$\begingroup\$ ⟪It seems to work, but I'm not sure if I handle all possible exceptions correctly.⟫ Try multiple base classes and virtual base classes. Try non-public base classes, and multiple copies of the same base class among the ancestors. \$\endgroup\$ – JDługosz Jul 1 '18 at 0:03
  • \$\begingroup\$ I'm not 100% sure, but this "unique_ptr, but copyable, and can hold any derived type, and can be visited via RTTI" smells an awful lot like std::any. Consider recasting your question (and your intuition about the desired behaviors of your class) in terms of a "visitable std::any." \$\endgroup\$ – Quuxplusone Jul 1 '18 at 3:07
  • \$\begingroup\$ @JDługosz Multiple bases and virtual bases seem to work. Non-public bases and duplicate bases produce compile-time errors, as expected. (But the errors are ugly, I'll add proper static_asserts.) \$\endgroup\$ – HolyBlackCat Jul 1 '18 at 12:46
  • \$\begingroup\$ @Quuxplusone I guess you're right, it's similar to a visitable std::any. I decided to present it as unique_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 from std::any, and I want to keep this behaviour). I think I'll leave the question mostly unchanged, but add a note about it resembling std::any. \$\endgroup\$ – HolyBlackCat Jul 1 '18 at 13:09
3
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ About get_instance: Good point, I'll replace it with a static object in DynStorage<T>::Implementation. About the derived<>(): The intent was to make an universal downcasting function, since static_cast doesn't work with virtual bases. About the alternative visiting syntax: Looks interesting, but the downside is that it requires writing MyFuncsBase:: each time. I guess I need to think what do I like more. \$\endgroup\$ – HolyBlackCat Jul 1 '18 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 limit DynStorage to only performing the work it's supposed to do. \$\endgroup\$ – Frank Jul 1 '18 at 12:43
  • \$\begingroup\$ func_base::copy_ requires derived<>() to function properly, but maybe I should put it into a separate header indeed. \$\endgroup\$ – HolyBlackCat Jul 1 '18 at 13:17

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.