QGIM: simplify new/deleteRow for tables

Conceptually, a table can deal with rows that are a pointer type,
and nullptr. We test for validity before accessing the row element, and
can return gracefully from e.g. data() or setData(). This is also
acceptable for lists, where a newly constructed row will hold a nullptr
element as the item. Client code that wants to initialize new rows
explicitly can provide their own protocol, or connect to rowsInserted()
and rowsAboutToBeRemoved().

However, we might end up violating the requirement that in a table, all
rows have to have the same number of columns: if the row-type is a
pointer to a struct or gadget, and we don't instantiate the element,
then we end up with an index that is de-facto invalid even though
it is in range. setData() cannot do anything but return false, and
data() can only return an invalid QVariant.
And if the row type is dynamically sized, and the new row is inserted
at index 0, then we end up reporting that the model has 0 columns, as we
use row 0 as the reference for the column count.

To fix this, implement new/deleteRow consistently for pointers (and
smart pointers): if the row type is a (smart) pointer holding
a default-constructible object, then we can instantiate a new object.
The deleteRow implementation takes care of deleting raw pointers (as
otherwise the `delete row` expression is invalid); for smart pointers,
the smart pointer takes care of memory management.

All other row types can be instantiated by default constructing. This
covers MultiColumn/SingleColumn wrappers as well, which can hold a
nullptr while still reporting the correct column count.

Remove the special handling from canInsertRows() - if newRow() is
available in the protocol, then we can insert rows.

But one ambiguity remains: if we operate on a copy of a container that
has pointers for rows, then adding new rows might allocate (in the
model's copy), resulting in a mixed situation. The client code can not
delete those new rows (they don't exist in the original container),
while the model must not delete rows it didn't create (those do exist
in the original container and might still be used).

Perhaps that cannot be solved, other than by documenting: either move
your range in the model (then ownership is clear), or pass a reference
or pointer the model (then ownership is also clear); or use smart
pointers for the row type.

Change-Id: I18a2e929473d118dcdb9d1f2ed67a7890f681974
Reviewed-by: Artem Dyomin <artem.dyomin@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-04-08 15:04:58 +02:00
parent 4d839093b4
commit 25f5bd5f66
3 changed files with 39 additions and 52 deletions

View File

@ -874,8 +874,7 @@ public:
{ // erase invalidates iterators { // erase invalidates iterators
const auto begin = QGenericItemModelDetails::pos(children, row); const auto begin = QGenericItemModelDetails::pos(children, row);
const auto end = std::next(begin, count); const auto end = std::next(begin, count);
if constexpr (rows_are_raw_pointers) that().deleteRemovedRows(begin, end);
that().deleteRemovedRows(begin, end);
children->erase(begin, end); children->erase(begin, end);
} }
// fix the parent in all children of the modified row, as the // fix the parent in all children of the modified row, as the
@ -939,16 +938,18 @@ public:
protected: protected:
~QGenericItemModelImpl() ~QGenericItemModelImpl()
{ {
if constexpr (rows_are_raw_pointers && !std::is_pointer_v<Range> // We delete row objects if we are not operating on a reference or pointer
&& !QGenericItemModelDetails::is_any_of<Range, std::reference_wrapper, // to a range, as in that case, the owner of the referenced/pointed to
std::shared_ptr, // range also owns the row entries.
QSharedPointer, // ### Problem: if we get a copy of a range (no matter if shared or not),
std::unique_ptr>()) { // then adding rows will create row objects in the model's copy, and the
// If data with rows as pointers was moved in, then we own it and // client can never delete those. But copied rows will be the same pointer,
// have to delete those rows. // which we must not delete (as we didn't create them).
using ref = decltype(std::forward<Range>(std::declval<range_type>())); if constexpr (protocol_traits::has_deleteRow && !std::is_pointer_v<Range>
if constexpr (std::is_rvalue_reference_v<ref>) && !QGenericItemModelDetails::is_any_of<Range, std::reference_wrapper>()) {
that().destroyOwnedModel(*m_data.model()); const auto begin = QGenericItemModelDetails::begin(*m_data.model());
const auto end = QGenericItemModelDetails::end(*m_data.model());
that().deleteRemovedRows(begin, end);
} }
} }
@ -958,10 +959,8 @@ protected:
// If we operate on dynamic columns and cannot resize a newly // If we operate on dynamic columns and cannot resize a newly
// constructed row, then we cannot insert. // constructed row, then we cannot insert.
return false; return false;
} else if constexpr (!one_dimensional_range && !rows_are_raw_pointers } else if constexpr (!protocol_traits::has_newRow) {
&& !protocol_traits::initializes_rows) { // We also cannot insert if we cannot create a new row element
// We also cannot insert if the row we have to create is not
// default-initialized by the protocol
return false; return false;
} else if constexpr (!range_features::has_insert_range } else if constexpr (!range_features::has_insert_range
&& !std::is_copy_constructible_v<row_type>) { && !std::is_copy_constructible_v<row_type>) {
@ -1414,19 +1413,13 @@ protected:
return empty_row; return empty_row;
} }
template <typename R>
void destroyOwnedModel(R &&range)
{
const auto begin = QGenericItemModelDetails::begin(range);
const auto end = QGenericItemModelDetails::end(range);
deleteRemovedRows(begin, end);
}
template <typename It, typename Sentinel> template <typename It, typename Sentinel>
void deleteRemovedRows(It &&begin, Sentinel &&end) void deleteRemovedRows(It &&begin, Sentinel &&end)
{ {
for (auto it = begin; it != end; ++it) if constexpr (tree_traits::has_deleteRow) {
this->protocol().deleteRow(*it); for (auto it = begin; it != end; ++it)
this->protocol().deleteRow(*it);
}
} }
void resetParentInChildren(range_type *children) void resetParentInChildren(range_type *children)
@ -1627,19 +1620,13 @@ protected:
return empty_row; return empty_row;
} }
template <typename R>
void destroyOwnedModel(R &&range)
{
const auto begin = QGenericItemModelDetails::begin(range);
const auto end = QGenericItemModelDetails::end(range);
for (auto it = begin; it != end; ++it)
delete *it;
}
template <typename It, typename Sentinel> template <typename It, typename Sentinel>
void deleteRemovedRows(It &&, Sentinel &&) void deleteRemovedRows(It &&begin, Sentinel &&end)
{ {
// nothing to do for tables and lists as we never create rows either if constexpr (Base::protocol_traits::has_deleteRow) {
for (auto it = begin; it != end; ++it)
this->protocol().deleteRow(*it);
}
} }
decltype(auto) rowDataImpl(const QModelIndex &index) const decltype(auto) rowDataImpl(const QModelIndex &index) const

View File

@ -72,8 +72,8 @@ namespace QGenericItemModelDetails
QExplicitlySharedDataPointer, QSharedDataPointer>; QExplicitlySharedDataPointer, QSharedDataPointer>;
template <typename T> template <typename T>
using is_owning_or_raw_pointer = std::disjunction<is_any_shared_ptr<T>, is_any_unique_ptr<T>, std::is_pointer<T>>; using is_owning_or_raw_pointer = std::disjunction<is_any_shared_ptr<T>, is_any_unique_ptr<T>,
std::is_pointer<T>>;
template <typename T> template <typename T>
static auto pointerTo(T&& t) { static auto pointerTo(T&& t) {
@ -354,10 +354,9 @@ namespace QGenericItemModelDetails
using row_type = typename range_traits<wrapped_t<Range>>::value_type; using row_type = typename range_traits<wrapped_t<Range>>::value_type;
template <typename R = row_type, template <typename R = row_type,
std::enable_if_t<std::is_destructible_v<wrapped_t<R>>, bool> = true, std::enable_if_t<std::conjunction_v<std::is_destructible<wrapped_t<R>>,
std::enable_if_t<is_owning_or_raw_pointer<R>::value, bool> = true> is_owning_or_raw_pointer<R>>, bool> = true>
auto newRow() -> decltype(R(new wrapped_t<R>)) auto newRow() -> decltype(R(new wrapped_t<R>)) {
{
if constexpr (is_any_of<R, std::shared_ptr>()) if constexpr (is_any_of<R, std::shared_ptr>())
return std::make_shared<wrapped_t<R>>(); return std::make_shared<wrapped_t<R>>();
else else
@ -365,13 +364,12 @@ namespace QGenericItemModelDetails
} }
template <typename R = row_type, template <typename R = row_type,
std::enable_if_t<std::negation_v<is_wrapped<R>>, bool> = true, std::enable_if_t<!is_owning_or_raw_pointer<R>::value, bool> = true>
std::enable_if_t<std::negation_v<is_validatable<R>>, bool> = true>
auto newRow() -> decltype(R{}) { return R{}; } auto newRow() -> decltype(R{}) { return R{}; }
template <typename R, template <typename R = row_type,
std::enable_if_t<std::is_pointer_v<R>, bool> = true> std::enable_if_t<std::is_pointer_v<std::remove_reference_t<R>>, bool> = true>
auto deleteRow(R row) -> decltype(delete(row)) { delete row; } auto deleteRow(R&& row) -> decltype(delete row) { delete row; }
}; };
template <typename Range, typename R = typename range_traits<wrapped_t<Range>>::value_type> template <typename Range, typename R = typename range_traits<wrapped_t<Range>>::value_type>
@ -485,7 +483,6 @@ namespace QGenericItemModelDetails
static constexpr bool has_setParentRow = protocol_setParentRow<protocol, row>(); static constexpr bool has_setParentRow = protocol_setParentRow<protocol, row>();
static constexpr bool has_mutable_childRows = protocol_mutable_childRows<protocol, row>(); static constexpr bool has_mutable_childRows = protocol_mutable_childRows<protocol, row>();
static constexpr bool initializes_rows = has_newRow && !is_any_of<protocol, ListProtocol>();
static constexpr bool is_default = is_any_of<protocol, ListProtocol, TableProtocol, DefaultTreeProtocol>(); static constexpr bool is_default = is_any_of<protocol, ListProtocol, TableProtocol, DefaultTreeProtocol>();
}; };

View File

@ -474,7 +474,7 @@ private:
Object row2; Object row2;
Object row3; Object row3;
std::list<Object *> listOfObjects = { std::list<Object *> listOfObjects = {
&row1, &row2, &row3 new Object, new Object, new Object
}; };
MetaObjectTuple mot1; MetaObjectTuple mot1;
@ -896,10 +896,12 @@ void tst_QGenericItemModel::ownership()
std::vector<Object *> objects { std::vector<Object *> objects {
object object
}; };
{ // model does not take ownership { // model takes ownership of its own copy of the vector (!)
QGenericItemModel modelOnCopy(objects); QGenericItemModel modelOnCopy(objects);
} }
QVERIFY(guard); QVERIFY(!guard);
objects[0] = new Object;
guard = objects[0];
{ // model does not take ownership { // model does not take ownership
QGenericItemModel modelOnPointer(&objects); QGenericItemModel modelOnPointer(&objects);
} }
@ -911,6 +913,7 @@ void tst_QGenericItemModel::ownership()
{ // model does take ownership { // model does take ownership
QGenericItemModel movedIntoModel(std::move(objects)); QGenericItemModel movedIntoModel(std::move(objects));
QCOMPARE(movedIntoModel.columnCount(), 2);
} }
QVERIFY(!guard); QVERIFY(!guard);
} }