2

Regardless of the fact that copying a unique_ptr makes sense or not*, I tried to implement this kind of class, simply wrapping a std::unique_ptr, and got into difficulty exactly where the copy is taken, in the case of a smart pointer to base and the stored object being a derived class.

A naive implementation of the copy constructor can be found all over the internet (data is the wrapped std::unique_ptr):

copyable_unique_ptr::copyable_unique_ptr(const copyable_unique_ptr& other)
  : data(std::make_unique(*other.get()) // invoke the class's copy constructor
{}

Problem here is, that due to the left out template arguments, is that the copy creates an instance of the type T, even if the real type is U : T. This leads to loss of information on a copy, and although I understand perfectly well why this happens here, I can't find a way around this.

Note that in the move case, there is no problem. The original pointer was created properly somewhere in user code, and moving it to a new owner doesn't modify the object's real type. To make a copy, you need more information.

Also note that a solution employing a clone function (thus infecting the type T's interface) is not what I would find to be acceptable.


*if you want a single owning pointer to a copyable resource this can make sense and it provides much more than what a scoped_ptr or auto_ptr would provide.

rubenvb
  • 74,642
  • 33
  • 187
  • 332
  • like this? https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern#Polymorphic_copy_construction – Hayt Oct 24 '16 at 10:34
  • @Hayt: please read my last sentence above the line. – rubenvb Oct 24 '16 at 10:35
  • I mean you would introduce a new type between `T` and `U`. Thus not infect T directly. – Hayt Oct 24 '16 at 10:36
  • Would non-member functions and code-duplication be an issue for you? ( Non-member functions can also be seen as part of T's interface though) If you would not want that, I don't think there will be a nice solution. I know of an ugly one though if you can edit T's copy constructor (with even more code duplication) – Hayt Oct 24 '16 at 10:39
  • Hmm it seems I found what I was looking for, apparently called a `value_ptr`. Example implementation here: https://bitbucket.org/martinhofernandes/wheels/src/17aee21522ce8d07c7a74b138e528fadf04d62ed/include/wheels/smart_ptr/value_ptr.h++?at=default&fileviewer=file-view-default. – rubenvb Oct 24 '16 at 10:46
  • Now I need to figure out a way for this to determine the correct type's copy constructor as the cloner when it's constructed from a certain subtype. Hmm, that might get messy when `reset` is involved :/ – rubenvb Oct 24 '16 at 12:32

1 Answers1

1

After some struggling with getting all the magic incantations right so that a good C++ compiler is satisfied with the code, and I was satisfied with the semantics, I present to you, a (very barebones) value_ptr, with both copy and move semantics. Important to remember is to use make_value<Derived> so it picks up the correct copy function, otherwise a copy will slice your object. I did not find an implementation of a deep_copy_ptr or value_ptr that actually had a mechanism to withstand slicing. This is a rough-edged implementation that misses things like the fine-grained reference handling or array specialization, but here it is nonetheless:

template <typename T>
static void* (*copy_constructor_copier())(void*)
{
  return [](void* other)
         { return static_cast<void*>(new T(*static_cast<T*>(other))); };
}

template<typename T>
class smart_copy
{
public:
  using copy_function_type = void*(*)(void*);

  explicit smart_copy() { static_assert(!std::is_abstract<T>::value, "Cannot default construct smart_copy for an abstract type."); }
  explicit smart_copy(copy_function_type copy_function) : copy_function(copy_function) {}
  smart_copy(const smart_copy& other) : copy_function(other.get_copy_function()) {}
  template<typename U>
  smart_copy(const smart_copy<U>& other) : copy_function(other.get_copy_function()) {}

  void* operator()(void* other) const { return copy_function(other); }
  copy_function_type get_copy_function() const { return copy_function; }

private:
  copy_function_type copy_function = copy_constructor_copier<T>();
};

template<typename T,
         typename Copier = smart_copy<T>,
         typename Deleter = std::default_delete<T>>
class value_ptr
{
  using pointer = std::add_pointer_t<T>;
  using element_type = std::remove_reference_t<T>;
  using reference = std::add_lvalue_reference_t<element_type>;
  using const_reference = std::add_const_t<reference>;
  using copier_type = Copier;
  using deleter_type = Deleter;

public:
  explicit constexpr value_ptr() = default;
  explicit constexpr value_ptr(std::nullptr_t) : value_ptr() {}
  explicit value_ptr(pointer p) : data{p, copier_type(), deleter_type()} {}

  ~value_ptr()
  {
    reset(nullptr);
  }

  explicit value_ptr(const value_ptr& other)
    : data{static_cast<pointer>(other.get_copier()(other.get())), other.get_copier(), other.get_deleter()} {}
  explicit value_ptr(value_ptr&& other)
    : data{other.get(), other.get_copier(), other.get_deleter()} { other.release(); }
  template<typename U, typename OtherCopier>
  value_ptr(const value_ptr<U, OtherCopier>& other)
    : data{static_cast<pointer>(other.get_copier().get_copy_function()(other.get())), other.get_copier(), other.get_deleter()} {}
  template<typename U, typename OtherCopier>
  value_ptr(value_ptr<U, OtherCopier>&& other)
    : data{other.get(), other.get_copier(), other.get_deleter()} { other.release(); }

  const value_ptr& operator=(value_ptr other) { swap(data, other.data); return *this; }
  template<typename U, typename OtherCopier, typename OtherDeleter>
  value_ptr& operator=(value_ptr<U, OtherCopier, OtherDeleter> other) { std::swap(data, other.data); return *this; }

  pointer operator->() { return get(); }
  const pointer operator->() const { return get(); }

  reference operator*() { return *get(); }
  const_reference operator*() const { return *get(); }

  pointer get() { return std::get<0>(data); }
  const pointer get() const { return std::get<0>(data); }

  copier_type& get_copier() { return std::get<1>(data); }
  const copier_type& get_copier() const { return std::get<1>(data); }
  deleter_type& get_deleter() { return std::get<2>(data); }
  const deleter_type& get_deleter() const { return std::get<2>(data); }

  void reset(pointer new_data)
  {
    if(get())
    {
      get_deleter()(get());
    }
    std::get<0>(data) = new_data;
  }

  pointer release() noexcept
  {
    pointer result = get();
    std::get<0>(data) = pointer();
    return result;
  }

private:
  std::tuple<pointer, copier_type, deleter_type> data = {nullptr, smart_copy<T>(), std::default_delete<T>()};
};

template<typename T, typename... ArgTypes>
value_ptr<T> make_value(ArgTypes&&... args)
{
  return value_ptr<T>(new T(std::forward<ArgTypes>(args)...));;
}

Code lives here and tests to show how it should work are here for everyone to see for themselves. Comments always welcome.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
rubenvb
  • 74,642
  • 33
  • 187
  • 332
  • Slicing protection is simply not worth the overhead that this class imposes on every `value_ptr` instance. – Nicol Bolas Oct 30 '16 at 14:38
  • @Nicol If you have an alternative solution to the problem of having only base class pointers and the possibility to make a non-slicing copy, I'm all ears. – rubenvb Oct 30 '16 at 21:24