Is it possible to implement a copyable_unique_ptr that is not affected by slicing?

梦想的初衷 提交于 2019-12-10 22:52:14

问题


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.


回答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.



来源:https://stackoverflow.com/questions/40216107/is-it-possible-to-implement-a-copyable-unique-ptr-that-is-not-affected-by-slicin

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!