Можно ли реализовать copyable_unique_ptr, на который не влияет нарезка?

Вне зависимости от того, имеет ли смысл копировать unique_ptr или нет*, я пытался реализовать такой класс, просто обернув std::unique_ptr, и столкнулся с трудностями именно там, где берется копия, в случае умного указателя на базу и хранимый объект является производным классом.

Наивную реализацию конструктора копирования можно найти по всему Интернету (data — это обернутый 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
{}

Проблема здесь в том, что из-за опущенных аргументов шаблона копия создает экземпляр типа T, даже если реальный тип U : T. Это приводит к потере информации на копии, и хотя я прекрасно понимаю, почему это происходит здесь, я не могу найти способ обойти это.

Обратите внимание, что в случае перемещения проблем нет. Исходный указатель был правильно создан где-то в пользовательском коде, и его перемещение к новому владельцу не изменяет реальный тип объекта. Чтобы сделать копию, вам нужна дополнительная информация.

Также обратите внимание, что решение, использующее функцию clone (таким образом заражающее интерфейс типа T), не является тем, что я считаю приемлемым.


* если вам нужен единственный указатель владельца на копируемый ресурс, это может иметь смысл, и он предоставляет гораздо больше, чем то, что могут предоставить scoped_ptr или auto_ptr.


person rubenvb    schedule 24.10.2016    source источник
comment
как это? en.wikipedia.org/wiki/   -  person Hayt    schedule 24.10.2016
comment
@Hayt: пожалуйста, прочитайте мое последнее предложение над строкой.   -  person rubenvb    schedule 24.10.2016
comment
Я имею в виду, что вы должны ввести новый тип между T и U. Таким образом, не заразить T напрямую.   -  person Hayt    schedule 24.10.2016
comment
Будут ли для вас проблемой функции, не являющиеся членами, и дублирование кода? (Хотя функции, не являющиеся членами, также можно рассматривать как часть интерфейса T). Если вы этого не хотите, я не думаю, что будет хорошее решение. Я знаю уродливого, если вы можете редактировать конструктор копирования T (с еще большим дублированием кода)   -  person Hayt    schedule 24.10.2016
comment
Хм, кажется, я нашел то, что искал, по-видимому, под названием value_ptr. Пример реализации здесь: bitbucket.org/martinhofernandes/wheels/src/.   -  person rubenvb    schedule 24.10.2016
comment
Теперь мне нужно найти способ определить конструктор копии правильного типа как клонер, когда он создан из определенного подтипа. Хм, это может стать грязным, когда задействован reset :/   -  person rubenvb    schedule 24.10.2016


Ответы (1)


После некоторых мучений с правильным получением всех магических заклинаний, чтобы хороший компилятор C++ был доволен кодом, а я был доволен семантикой, я представляю вам (очень простой) value_ptr с семантикой копирования и перемещения. Важно помнить, что используйте make_value<Derived>, чтобы он подбирал правильную функцию копирования, иначе копия нарежет ваш объект. Я не нашел реализации deep_copy_ptr или value_ptr, в которой действительно был бы механизм, позволяющий противостоять нарезке. Это грубая реализация, в которой отсутствуют такие вещи, как мелкозернистая обработка ссылок или специализация массива, но, тем не менее, она есть:

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)...));;
}

Код находится здесь и тестируется, чтобы показать, как он должно работать здесь для всех сами посмотрите. Комментарии всегда приветствуются.

person rubenvb    schedule 30.10.2016
comment
Защита от нарезки просто не стоит тех накладных расходов, которые этот класс налагает на каждый экземпляр value_ptr. - person Nicol Bolas; 30.10.2016
comment
@Nicol Если у вас есть альтернативное решение проблемы наличия только указателей базового класса и возможности сделать копию без нарезки, я внимательно слушаю. - person rubenvb; 31.10.2016