QList полиморфного класса с копированием при записи?

Я пытаюсь создать QList полиморфного типа, который по-прежнему использует Qt неявный обмен< /а>.

Мой конкретный вариант использования — передача элементов, хранящихся в QList, в QtConcurrent::mapped . Все элементы происходят от базового класса, который определяет виртуальную функцию, которую будет вызывать QtConcurrent::mapped. Большая часть хранимых данных будет специфичной для дочернего класса. Эти элементы можно редактировать после начала потоковой обработки, оставляя мне два основных варианта: заблокировать или скопировать данные. Я не хочу вставлять блокировки, потому что это устранит большую часть цели использования дополнительных потоков. Кроме того, создание полных копий моих данных также кажется весьма нежелательным. Вместо этого я хотел бы использовать неявное совместное использование Qt только для создания копий элементов данных, которые я изменяю, однако я не могу создать QList полиморфного типа, который по-прежнему использует неявное совместное использование.

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

QList<Base> list;
Derived derived_obj;
list.append(derived_obj); // this fails

Однако добавление дочернего класса в QList родительского класса не будет работать, и in-c-qt">стандартный ответ заключается в том, чтобы вместо этого использовать QList QSharedPointers для базового класса, который примет добавление указателя на дочерний класс.

QList<QSharedPointer<Base> > pointer_list;
QSharedPointer<Derived> derived_pointer;
pointer_list.append(derived_pointer); // this works but there is no copy-on-write

Если я использую QList QSharedPointers, будет скопирован именно QSharedPointer, а не мой полиморфный класс, а это означает, что я потерял функциональность копирования при записи, которую хотел бы.

Я также рассматривал использование QList из QSharedDataPointers.

QList<QSharedDataPointer<Base> > data_pointer_list;
QSharedDataPointer<Derived> derived_data_pointer;
list.append(derived_data_pointer); // this fails

Однако, как и сам QList, QSharedDataPointers, похоже, не принимает полиморфные типы.


person Chris    schedule 16.06.2017    source источник


Ответы (1)


Это не удается:

QList<QSharedDataPointer<Base>> list;
QSharedDataPointer<Derived> derived(new Derived);
list.append(derived);

Примечание. Альтернативным подходом к приведенному ниже подходу было бы объединение PolymorphicShared и PolymorphicSharedBase для добавления поддержки полиморфизма непосредственно в QSharedDataPointer, без особых требований к типу, производному от QSharedData (например, ему не нужно было бы явно поддерживать clone). Это требует немного больше работы. Ниже приведен только один рабочий подход.

QSharedDataPointer действительно является ответом, который вы ищете, и определенно может содержать полиморфный QSharedData. Вам нужно разделить тип на иерархию, основанную на QSharedData, и другую параллельную иерархию, обертывающую QSharedDataPointer. QSharedDataPointer обычно не предназначен для непосредственного использования конечным пользователем класса. Это деталь реализации, полезная при реализации неявно разделяемого класса.

Ради эффективности QSharedDataPointer — это небольшой тип, который можно перемещать на уровне битов. Это довольно эффективно при хранении в контейнерах всех видов, особенно в контейнерах Qt, которые могут использовать признаки типа, чтобы знать об этом свойстве. Размер класса, использующего QSharedDataPointer, обычно удваивается, если мы делаем его полиморфным, поэтому лучше этого не делать. Конечно, тип данных, на который указывают, может быть полиморфным.

Во-первых, давайте определим довольно универсальный базовый класс PIMPL, на котором вы будете строить иерархию. Класс PIMPL можно сбросить в поток отладки и клонировать.

// https://github.com/KubaO/stackoverflown/tree/master/questions/implicit-list-44593216
#include <QtCore>
#include <type_traits>

class PolymorphicSharedData : public QSharedData {
public:
   virtual PolymorphicSharedData * clone() const = 0;
   virtual QDebug dump(QDebug) const = 0;
   virtual ~PolymorphicSharedData() {}
};

Типы xxxData являются PIMPL и не предназначены для использования конечным пользователем. Пользователь должен использовать сам тип xxx. Затем этот общий тип обертывает полиморфный PIMPL и использует QSharedDataPointer для хранения PIMPL. Он раскрывает методы PIMPL.

Сам тип не является полиморфным, чтобы сэкономить на размере указателя виртуальной таблицы. Функция as() действует так же, как dynamic_cast(), перенаправляя полиморфизм в PIMPL.

class PolymorphicShared {
protected:
   QSharedDataPointer<PolymorphicSharedData> d_ptr;
   PolymorphicShared(PolymorphicSharedData * d) : d_ptr(d) {}
public:
   PolymorphicShared() = default;
   PolymorphicShared(const PolymorphicShared & o) = default;
   PolymorphicShared & operator=(const PolymorphicShared &) = default;
   QDebug dump(QDebug dbg) const { return d_ptr->dump(dbg); }
   template <class T> typename
   std::enable_if<std::is_pointer<T>::value, typename
   std::enable_if<!std::is_const<typename std::remove_pointer<T>::type>::value, T>::type>
   ::type as() {
      if (dynamic_cast<typename std::remove_pointer<T>::type::PIMPL*>(d_ptr.data()))
         return static_cast<T>(this);
      return {};
   }
   template <class T> typename
   std::enable_if<std::is_pointer<T>::value, typename
   std::enable_if<std::is_const<typename std::remove_pointer<T>::type>::value, T>::type>
   ::type as() const {
      if (dynamic_cast<const typename std::remove_pointer<T>::type::PIMPL*>(d_ptr.data()))
         return static_cast<T>(this);
      return {};
   }
   template <class T> typename
   std::enable_if<std::is_reference<T>::value, typename
   std::enable_if<!std::is_const<typename std::remove_reference<T>::type>::value, T>::type>
   ::type as() {
      Q_UNUSED(dynamic_cast<typename std::remove_reference<T>::type::PIMPL&>(*d_ptr));
      return static_cast<T>(*this);
   }
   template <class T> typename
   std::enable_if<std::is_reference<T>::value, typename
   std::enable_if<std::is_const<typename std::remove_reference<T>::type>::value, T>::type>
   ::type as() const {
      Q_UNUSED(dynamic_cast<const typename std::remove_reference<T>::type::PIMPL&>(*d_ptr));
      return static_cast<T>(*this);
   }
   int ref() const { return d_ptr ? d_ptr->ref.load() : 0; }
};

QDebug operator<<(QDebug dbg, const PolymorphicShared & val) {
   return val.dump(dbg);
}

Q_DECLARE_TYPEINFO(PolymorphicShared, Q_MOVABLE_TYPE);

#define DECLARE_TYPEINFO(concreteType) Q_DECLARE_TYPEINFO(concreteType, Q_MOVABLE_TYPE)

template <> PolymorphicSharedData * QSharedDataPointer<PolymorphicSharedData>::clone() {
   return d->clone();
}

Помощник упрощает использование абстрактного базового класса с производными типами данных. Он приводит d-ptr к правильному производному типу PIMPL и передает аргументы конструктора конструктору PIMPL.

template <class Data, class Base = PolymorphicShared> class PolymorphicSharedBase : public Base {
   friend class PolymorphicShared;
protected:
   using PIMPL = typename std::enable_if<std::is_base_of<PolymorphicSharedData, Data>::value, Data>::type;
   PIMPL * d() { return static_cast<PIMPL*>(&*this->d_ptr); }
   const PIMPL * d() const { return static_cast<const PIMPL*>(&*this->d_ptr); }
   PolymorphicSharedBase(PolymorphicSharedData * d) : Base(d) {}
   template <typename T> static typename std::enable_if<std::is_constructible<T>::value, T*>::type
   construct() { return new T(); }
   template <typename T> static typename std::enable_if<!std::is_constructible<T>::value, T*>::type
   construct() { return nullptr; }
public:
   using Base::Base;
   template<typename ...Args,
            typename = typename std::enable_if<std::is_constructible<Data, Args...>::value>::type
            > PolymorphicSharedBase(Args&&... args) :
      Base(static_cast<PolymorphicSharedData*>(new Data(std::forward<Args>(args)...))) {}
   PolymorphicSharedBase() : Base(construct<Data>()) {}
};

Теперь достаточно просто иметь параллельную иерархию типов PIMPL и их носителей. Во-первых, базовый абстрактный тип в нашей иерархии, который добавляет два метода. Обратите внимание, как PolymorphicSharedBase добавляет метод доступа d() правильного типа.

class MyAbstractTypeData : public PolymorphicSharedData {
public:
   virtual void gurgle() = 0;
   virtual int gargle() const = 0;
};

class MyAbstractType : public PolymorphicSharedBase<MyAbstractTypeData> {
public:
   using PolymorphicSharedBase::PolymorphicSharedBase;
   void gurgle() { d()->gurgle(); }
   int gargle() const { return d()->gargle(); }
};
DECLARE_TYPEINFO(MyAbstractType);

Затем конкретный тип, который не добавляет новых методов:

class FooTypeData : public MyAbstractTypeData {
protected:
   int m_foo = 0;
public:
   FooTypeData() = default;
   FooTypeData(int data) : m_foo(data) {}
   void gurgle() override { m_foo++; }
   int gargle() const override { return m_foo; }
   MyAbstractTypeData * clone() const override { return new FooTypeData(*this); }
   QDebug dump(QDebug dbg) const override {
      return dbg << "FooType-" << ref << ":" << m_foo;
   }
};

using FooType = PolymorphicSharedBase<FooTypeData, MyAbstractType>;
DECLARE_TYPEINFO(FooType);

И еще один тип, добавляющий методы.

class BarTypeData : public FooTypeData {
protected:
   int m_bar = 0;
public:
   BarTypeData() = default;
   BarTypeData(int data) : m_bar(data) {}
   MyAbstractTypeData * clone() const override { return new BarTypeData(*this); }
   QDebug dump(QDebug dbg) const override {
      return dbg << "BarType-" << ref << ":" << m_foo << "," << m_bar;
   }
   virtual void murgle() { m_bar++; }
};

class BarType : public PolymorphicSharedBase<BarTypeData, FooType> {
public:
   using PolymorphicSharedBase::PolymorphicSharedBase;
   void murgle() { d()->murgle(); }
};
DECLARE_TYPEINFO(BarType);

Мы хотим убедиться, что метод as() выбрасывает по мере необходимости:

template <typename F> bool is_bad_cast(F && fun) {
   try { fun(); } catch (std::bad_cast) { return true; }
   return false;
}

Использование неявно разделяемых типов ничем не отличается от использования собственных таких типов Qt. Мы также можем использовать as вместо dynamic_cast.

int main() {
   Q_ASSERT(sizeof(FooType) == sizeof(void*));
   MyAbstractType a;
   Q_ASSERT(!a.as<FooType*>());
   FooType foo;
   Q_ASSERT(foo.as<FooType*>());
   a = foo;
   Q_ASSERT(a.ref() == 2);
   Q_ASSERT(a.as<const FooType*>());
   Q_ASSERT(a.ref() == 2);
   Q_ASSERT(a.as<FooType*>());
   Q_ASSERT(a.ref() == 1);
   MyAbstractType a2(foo);
   Q_ASSERT(a2.ref() == 2);

   QList<MyAbstractType> list1{FooType(3), BarType(8)};
   auto list2 = list1;
   qDebug() << "After copy:         " << list1 << list2;
   list2.detach();
   qDebug() << "After detach:       " << list1 << list2;
   list1[0].gurgle();
   qDebug() << "After list1[0] mod: " << list1 << list2;

   Q_ASSERT(list2[1].as<BarType*>());
   list2[1].as<BarType&>().murgle();
   qDebug() << "After list2[1] mod: " << list1 << list2;

   Q_ASSERT(!list2[0].as<BarType*>());
   Q_ASSERT(is_bad_cast([&]{ list2[0].as<BarType&>(); }));

   auto const list3 = list1;
   Q_ASSERT(!list3[0].as<const BarType*>());
   Q_ASSERT(is_bad_cast([&]{ list3[0].as<const BarType&>(); }));
}

Вывод:

After copy:          (FooType-1:3, BarType-1:0,8) (FooType-1:3, BarType-1:0,8)
After detach:        (FooType-2:3, BarType-2:0,8) (FooType-2:3, BarType-2:0,8)
After list1[0] mod:  (FooType-1:4, BarType-2:0,8) (FooType-1:3, BarType-2:0,8)
After list2[1] mod:  (FooType-1:4, BarType-1:0,8) (FooType-1:3, BarType-1:0,9)

Копия списка была неглубокой, и сами элементы не были скопированы: все счетчики ссылок равны 1. После отсоединения все элементы данных были скопированы, но, поскольку они неявно совместно используются, они только увеличили свои счетчики ссылок. Наконец, после того, как элемент был изменен, он автоматически отсоединяется, и счетчик ссылок возвращается к 1.

person Kuba hasn't forgotten Monica    schedule 16.06.2017