Add prepend optimization to QCommonArrayOps

Introduced prepend optimization logic to QCommonArrayOps.
Trying to rely on original QList behavior

Task-number: QTBUG-84320
Change-Id: I46e6797b4edad804a3e3edb58307c9e96990fe01
Reviewed-by: Sona Kurazyan <sona.kurazyan@qt.io>
This commit is contained in:
Andrei Golubev 2020-07-20 16:15:18 +02:00
parent e35d0ae0cc
commit f9bb3aa5ce
4 changed files with 710 additions and 72 deletions

View File

@ -44,10 +44,13 @@
#include <QtCore/qarraydata.h>
#include <QtCore/qcontainertools_impl.h>
#include <algorithm>
#include <new>
#include <string.h>
#include <utility>
#include <iterator>
#include <tuple>
#include <type_traits>
QT_BEGIN_NAMESPACE
@ -147,6 +150,7 @@ struct QArrayExceptionSafetyPrimitives
{
It *iter;
It end;
It intermediate;
Destructor(It &it) noexcept(std::is_nothrow_copy_constructible_v<It>)
: iter(std::addressof(it)), end(it)
@ -155,6 +159,10 @@ struct QArrayExceptionSafetyPrimitives
{
iter = std::addressof(end);
}
void freeze() noexcept(std::is_nothrow_copy_constructible_v<It>)
{
intermediate = *iter; iter = std::addressof(intermediate);
}
~Destructor() noexcept(std::is_nothrow_destructible_v<T>)
{
// Step is either 1 or -1 and depends on the interposition of *iter
@ -218,6 +226,9 @@ struct QArrayExceptionSafetyPrimitives
// Tags for compile-time dispatch based on backwards vs forward growing policy
struct GrowsForwardTag {};
struct GrowsBackwardsTag {};
template<typename> struct InverseTag;
template<> struct InverseTag<GrowsForwardTag> { using tag = GrowsBackwardsTag; };
template<> struct InverseTag<GrowsBackwardsTag> { using tag = GrowsForwardTag; };
QT_WARNING_PUSH
#if defined(Q_CC_GNU) && Q_CC_GNU >= 700
@ -244,31 +255,8 @@ public:
this->size = qsizetype(newSize);
}
template<typename iterator>
void copyAppend(iterator b, iterator e, QtPrivate::IfIsForwardIterator<iterator> = true)
{
Q_ASSERT(this->isMutable() || b == e);
Q_ASSERT(!this->isShared() || b == e);
Q_ASSERT(std::distance(b, e) >= 0 && qsizetype(std::distance(b, e)) <= this->freeSpaceAtEnd());
T *iter = this->end();
for (; b != e; ++iter, ++b) {
new (iter) T(*b);
++this->size;
}
}
void copyAppend(const T *b, const T *e)
{ insert(this->end(), b, e); }
void moveAppend(T *b, T *e)
{ copyAppend(b, e); }
void copyAppend(size_t n, parameter_type t)
{ insert(this->end(), n, t); }
template <typename ...Args>
void emplaceBack(Args&&... args) { this->emplace(this->end(), T(std::forward<Args>(args)...)); }
{ insert(this->end(), b, e); }
void truncate(size_t newSize)
{
@ -397,9 +385,9 @@ public:
} else {
// Preserve the value, because it might be a reference to some part of the moved chunk
T t(std::forward<Args>(args)...);
auto oldBegin = this->begin();
--this->ptr;
::memmove(static_cast<void *>(this->begin()), static_cast<void *>(oldBegin),
(where - oldBegin) * sizeof(T));
*(where - 1) = t;
@ -504,23 +492,6 @@ struct QGenericArrayOps
} while (size_t(++this->size) != newSize);
}
template<typename iterator>
void copyAppend(iterator b, iterator e, QtPrivate::IfIsForwardIterator<iterator> = true)
{
Q_ASSERT(this->isMutable() || b == e);
Q_ASSERT(!this->isShared() || b == e);
Q_ASSERT(std::distance(b, e) >= 0 && qsizetype(std::distance(b, e)) <= this->freeSpaceAtEnd());
T *iter = this->end();
for (; b != e; ++iter, ++b) {
new (iter) T(*b);
++this->size;
}
}
void copyAppend(const T *b, const T *e)
{ insert(this->end(), b, e); }
void moveAppend(T *b, T *e)
{
Q_ASSERT(this->isMutable() || b == e);
@ -537,15 +508,6 @@ struct QGenericArrayOps
this->size += copier.move(b, e);
}
void copyAppend(size_t n, parameter_type t)
{ insert(this->end(), n, t); }
template <typename ...Args>
void emplaceBack(Args&&... args)
{
this->emplace(this->end(), std::forward<Args>(args)...);
}
void truncate(size_t newSize)
{
Q_ASSERT(this->isMutable());
@ -854,7 +816,6 @@ struct QGenericArrayOps
} while (b != e);
}
void assign(T *b, T *e, parameter_type t)
{
Q_ASSERT(b <= e);
@ -1054,6 +1015,267 @@ struct QCommonArrayOps : QArrayOpsSelector<T>::Type
using iterator = typename Base::iterator;
using const_iterator = typename Base::const_iterator;
private:
using Self = QCommonArrayOps<T>;
// Tag dispatched helper functions
inline void adjustPointer(GrowsBackwardsTag, size_t distance) noexcept
{
this->ptr -= distance;
}
inline void adjustPointer(GrowsForwardTag, size_t distance) noexcept
{
this->ptr += distance;
}
qsizetype freeSpace(GrowsBackwardsTag) const noexcept { return this->freeSpaceAtBegin(); }
qsizetype freeSpace(GrowsForwardTag) const noexcept { return this->freeSpaceAtEnd(); }
struct RelocatableMoveOps
{
// The necessary evil. Performs move "to the left" when grows backwards and
// move "to the right" when grows forward
template<typename GrowthTag>
static void moveInGrowthDirection(GrowthTag tag, Self *this_, size_t futureGrowth)
{
Q_ASSERT(this_->isMutable());
Q_ASSERT(!this_->isShared());
Q_ASSERT(futureGrowth <= size_t(this_->freeSpace(tag)));
const auto oldBegin = this_->begin();
this_->adjustPointer(tag, futureGrowth);
// Note: move all elements!
::memmove(static_cast<void *>(this_->begin()), static_cast<const void *>(oldBegin),
this_->size * sizeof(T));
}
};
struct GenericMoveOps
{
template <typename ...Args>
static void createInPlace(T *where, Args&&... args)
{
new (where) T(std::forward<Args>(args)...);
}
template <typename ...Args>
static void createInPlace(std::reverse_iterator<iterator> where, Args&&... args)
{
// Note: instead of std::addressof(*where)
createInPlace(where.base() - 1, std::forward<Args>(args)...);
}
// Moves non-pod data range. Handles overlapping regions. By default, expect
// this method to perform move to the _right_. When move to the _left_ is
// needed, use reverse iterators.
template<typename GrowthTag, typename It>
static void moveNonPod(GrowthTag, Self *this_, It where, It begin, It end)
{
Q_ASSERT(begin <= end);
Q_ASSERT(where > begin); // move to the right
using Destructor = typename QArrayExceptionSafetyPrimitives<T>::template Destructor<It>;
auto start = where + std::distance(begin, end);
auto e = end;
Destructor destroyer(start); // Keep track of added items
auto [oldRangeEnd, overlapStart] = std::minmax(where, end);
// step 1. move-initialize elements in uninitialized memory region
while (start != overlapStart) {
--e;
createInPlace(start - 1, std::move_if_noexcept(*e));
// change tracked iterator only after creation succeeded - avoid
// destructing partially constructed objects if exception thrown
--start;
}
// step 2. move assign over existing elements in the overlapping
// region (if there's an overlap)
while (e != begin) {
--start, --e;
*start = std::move_if_noexcept(*e);
}
// re-created the range. now there is an initialized memory region
// somewhere in the allocated area. if something goes wrong, we must
// clean it up, so "freeze" the position for now (cannot commit yet)
destroyer.freeze();
// step 3. destroy elements in the old range
const qsizetype originalSize = this_->size;
start = oldRangeEnd; // mind the possible gap, have to re-assign
while (start != begin) {
// Exceptions or not, dtor called once per instance
--this_->size;
(--start)->~T();
}
destroyer.commit();
// restore old size as we consider data move to be done, the pointer
// still has to be adjusted!
this_->size = originalSize;
}
// Super inefficient function. The necessary evil. Performs move "to
// the left" when grows backwards and move "to the right" when grows
// forward
template<typename GrowthTag>
static void moveInGrowthDirection(GrowthTag tag, Self *this_, size_t futureGrowth)
{
Q_ASSERT(this_->isMutable());
Q_ASSERT(!this_->isShared());
Q_ASSERT(futureGrowth <= size_t(this_->freeSpace(tag)));
if (futureGrowth == 0) // avoid doing anything if there's no need
return;
// Note: move all elements!
if constexpr (std::is_same_v<std::decay_t<GrowthTag>, GrowsBackwardsTag>) {
auto where = this_->begin() - futureGrowth;
// here, magic happens. instead of having move to the right, we'll
// have move to the left by using reverse iterators
moveNonPod(tag, this_,
std::make_reverse_iterator(where + this_->size), // rwhere
std::make_reverse_iterator(this_->end()), // rbegin
std::make_reverse_iterator(this_->begin())); // rend
this_->ptr = where;
} else {
auto where = this_->begin() + futureGrowth;
moveNonPod(tag, this_, where, this_->begin(), this_->end());
this_->ptr = where;
}
}
};
// Moves all elements in a specific direction by moveSize if available
// free space at one of the ends is smaller than required. Free space
// becomes available at the beginning if grows backwards and at the end
// if grows forward
template<typename GrowthTag>
qsizetype prepareFreeSpace(GrowthTag tag, size_t required, size_t moveSize)
{
Q_ASSERT(this->isMutable() || required == 0);
Q_ASSERT(!this->isShared() || required == 0);
Q_ASSERT(required <= this->constAllocatedCapacity() - this->size);
using MoveOps = std::conditional_t<QTypeInfo<T>::isRelocatable,
RelocatableMoveOps,
GenericMoveOps>;
// if free space at the end is not enough, we need to move the data,
// move is performed in an inverse direction
if (size_t(freeSpace(tag)) < required) {
using MoveTag = typename InverseTag<std::decay_t<GrowthTag>>::tag;
MoveOps::moveInGrowthDirection(MoveTag{}, this, moveSize);
if constexpr (std::is_same_v<MoveTag, GrowsBackwardsTag>) {
return -qsizetype(moveSize); // moving data to the left
} else {
return qsizetype(moveSize); // moving data to the right
}
}
return 0;
}
// Helper wrapper that adjusts passed iterators along with moving the data
// around. The adjustment is necessary when iterators point inside the
// to-be-moved range
template<typename GrowthTag, typename It>
void prepareFreeSpace(GrowthTag tag, size_t required, size_t moveSize, It &b, It &e) {
// Returns whether passed iterators are inside [this->begin(), this->end()]
const auto iteratorsInRange = [&] (const It &first, const It &last) {
using DecayedIt = std::decay_t<It>;
using RemovedConstVolatileIt = std::remove_cv_t<It>;
constexpr bool selfIterator =
// if passed type is an iterator type:
std::is_same_v<DecayedIt, iterator>
|| std::is_same_v<DecayedIt, const_iterator>
// if passed type is a pointer type:
|| std::is_same_v<RemovedConstVolatileIt, T *>
|| std::is_same_v<RemovedConstVolatileIt, const T *>
|| std::is_same_v<RemovedConstVolatileIt, const volatile T *>;
if constexpr (selfIterator) {
return (first >= this->begin() && last <= this->end());
}
return false;
};
const bool inRange = iteratorsInRange(b, e);
const auto diff = prepareFreeSpace(tag, required, moveSize);
if (inRange) {
std::advance(b, diff);
std::advance(e, diff);
}
}
size_t moveSizeForAppend(size_t)
{
// Qt5 QList in append: make 100% free space at end if not enough space
// Now:
return this->freeSpaceAtBegin();
}
size_t moveSizeForPrepend(size_t required)
{
// Qt5 QList in prepend: make 33% of all space at front if not enough space
// Now:
qsizetype space = this->allocatedCapacity() / 3;
space = qMax(space, qsizetype(required)); // in case required > 33% of all space
return qMin(space, this->freeSpaceAtEnd());
}
// Helper functions that reduce usage boilerplate
void prepareSpaceForAppend(size_t required)
{
prepareFreeSpace(GrowsForwardTag{}, required, moveSizeForAppend(required));
}
void prepareSpaceForPrepend(size_t required)
{
prepareFreeSpace(GrowsBackwardsTag{}, required, moveSizeForPrepend(required));
}
template<typename It>
void prepareSpaceForAppend(It &b, It &e, size_t required)
{
prepareFreeSpace(GrowsForwardTag{}, required, moveSizeForAppend(required), b, e);
}
template<typename It>
void prepareSpaceForPrepend(It &b, It &e, size_t required)
{
prepareFreeSpace(GrowsBackwardsTag{}, required, moveSizeForPrepend(required), b, e);
}
// Tells how much of the given size to insert at the beginning of the
// container. This is insert-specific helper function
qsizetype sizeToInsertAtBegin(const T *const where, qsizetype size)
{
Q_ASSERT(size_t(size) <= this->allocatedCapacity() - this->size);
Q_ASSERT(where >= this->begin() && where <= this->end()); // in range
const auto freeAtBegin = this->freeSpaceAtBegin();
const auto freeAtEnd = this->freeSpaceAtEnd();
// Idea: * if enough space on both sides, try to affect less elements
// * if enough space on one of the sides, use only that side
// * otherwise, split between front and back (worst case)
if (freeAtBegin >= size && freeAtEnd >= size) {
if (where - this->begin() < this->end() - where) {
return size;
} else {
return 0;
}
} else if (freeAtBegin >= size) {
return size;
} else if (freeAtEnd >= size) {
return 0;
} else {
return size - freeAtEnd;
}
}
public:
// Returns whether reallocation is desirable before adding more elements
// into the container. This is a helper function that one can use to
// theoretically improve average operations performance. Ignoring this
@ -1088,6 +1310,177 @@ struct QCommonArrayOps : QArrayOpsSelector<T>::Type
// Now: no free space OR not enough space on either of the sides (bad perf. case)
return (freeAtBegin + freeAtEnd) < n || (freeAtBegin < n && freeAtEnd < n);
}
// using Base::truncate;
// using Base::destroyAll;
// using Base::assign;
// using Base::compare;
void appendInitialize(size_t newSize)
{
Q_ASSERT(this->isMutable());
Q_ASSERT(!this->isShared());
Q_ASSERT(newSize > size_t(this->size));
Q_ASSERT(newSize <= this->allocatedCapacity());
// Since this is mostly an initialization function, do not follow append
// logic of space arrangement. Instead, only prepare as much free space
// as needed for this specific operation
const size_t n = newSize - this->size;
prepareFreeSpace(GrowsForwardTag{}, n,
qMin(n, size_t(this->freeSpaceAtBegin()))); // ### perf. loss
Base::appendInitialize(newSize);
}
void copyAppend(const T *b, const T *e)
{
Q_ASSERT(this->isMutable() || b == e);
Q_ASSERT(!this->isShared() || b == e);
Q_ASSERT(b <= e);
Q_ASSERT(size_t(e - b) <= this->allocatedCapacity() - this->size);
prepareSpaceForAppend(b, e, e - b); // ### perf. loss
Base::insert(GrowsForwardTag{}, this->end(), b, e);
}
template<typename It>
void copyAppend(It b, It e, QtPrivate::IfIsForwardIterator<It> = true,
QtPrivate::IfIsNotSame<std::decay_t<It>, iterator> = true,
QtPrivate::IfIsNotSame<std::decay_t<It>, const_iterator> = true)
{
Q_ASSERT(this->isMutable() || b == e);
Q_ASSERT(!this->isShared() || b == e);
const qsizetype distance = std::distance(b, e);
Q_ASSERT(distance >= 0 && size_t(distance) <= this->allocatedCapacity() - this->size);
prepareSpaceForAppend(b, e, distance); // ### perf. loss
T *iter = this->end();
for (; b != e; ++iter, ++b) {
new (iter) T(*b);
++this->size;
}
}
void moveAppend(T *b, T *e)
{
Q_ASSERT(this->isMutable() || b == e);
Q_ASSERT(!this->isShared() || b == e);
Q_ASSERT(b <= e);
Q_ASSERT(size_t(e - b) <= this->allocatedCapacity() - this->size);
prepareSpaceForAppend(b, e, e - b); // ### perf. loss
Base::moveAppend(b, e);
}
void copyAppend(size_t n, parameter_type t)
{
Q_ASSERT(!this->isShared() || n == 0);
Q_ASSERT(this->allocatedCapacity() - size_t(this->size) >= n);
// Preserve the value, because it might be a reference to some part of the moved chunk
T tmp(t);
prepareSpaceForAppend(n); // ### perf. loss
Base::insert(GrowsForwardTag{}, this->end(), n, tmp);
}
void insert(T *where, const T *b, const T *e)
{
Q_ASSERT(this->isMutable() || (b == e && where == this->end()));
Q_ASSERT(!this->isShared() || (b == e && where == this->end()));
Q_ASSERT(where >= this->begin() && where <= this->end());
Q_ASSERT(b <= e);
Q_ASSERT(e <= where || b > this->end() || where == this->end()); // No overlap or append
Q_ASSERT(size_t(e - b) <= this->allocatedCapacity() - this->size);
if (where == this->begin()) { // prepend case - special space arrangement
prepareSpaceForPrepend(b, e, e - b); // ### perf. loss
Base::insert(GrowsBackwardsTag{}, this->begin(), b, e);
return;
} else if (where == this->end()) { // append case - special space arrangement
copyAppend(b, e);
return;
}
// Insert elements based on the divided distance. Good case: only 1
// insert happens (either to the front part or to the back part). Bad
// case: both inserts happen, meaning that we touch all N elements in
// the container (this should be handled "outside" by ensuring enough
// free space by reallocating more frequently)
const auto k = sizeToInsertAtBegin(where, e - b);
Base::insert(GrowsBackwardsTag{}, where, b, b + k);
Base::insert(GrowsForwardTag{}, where, b + k, e);
}
void insert(T *where, size_t n, parameter_type t)
{
Q_ASSERT(!this->isShared() || (n == 0 && where == this->end()));
Q_ASSERT(where >= this->begin() && where <= this->end());
Q_ASSERT(this->allocatedCapacity() - size_t(this->size) >= n);
if (where == this->begin()) { // prepend case - special space arrangement
// Preserve the value, because it might be a reference to some part of the moved chunk
T tmp(t);
prepareSpaceForPrepend(n); // ### perf. loss
Base::insert(GrowsBackwardsTag{}, this->begin(), n, tmp);
return;
} else if (where == this->end()) { // append case - special space arrangement
copyAppend(n, t);
return;
}
// Insert elements based on the divided distance. Good case: only 1
// insert happens (either to the front part or to the back part). Bad
// case: both inserts happen, meaning that we touch all N elements in
// the container (this should be handled "outside" by ensuring enough
// free space by reallocating more frequently)
const auto beginSize = sizeToInsertAtBegin(where, qsizetype(n));
Base::insert(GrowsBackwardsTag{}, where, beginSize, t);
Base::insert(GrowsForwardTag{}, where, qsizetype(n) - beginSize, t);
}
template <typename iterator, typename ...Args>
void emplace(iterator where, Args&&... args)
{
Q_ASSERT(!this->isShared());
Q_ASSERT(where >= this->begin() && where <= this->end());
Q_ASSERT(this->allocatedCapacity() - this->size >= 1);
// Qt5 QList in insert(1): try to move less data around
// Now:
const bool shouldInsertAtBegin = (where - this->begin()) < (this->end() - where)
|| this->freeSpaceAtEnd() <= 0;
if (this->freeSpaceAtBegin() > 0 && shouldInsertAtBegin) {
Base::emplace(GrowsBackwardsTag{}, where, std::forward<Args>(args)...);
} else {
Base::emplace(GrowsForwardTag{}, where, std::forward<Args>(args)...);
}
}
template <typename ...Args>
void emplaceBack(Args&&... args)
{
this->emplace(this->end(), std::forward<Args>(args)...);
}
void erase(T *b, T *e)
{
Q_ASSERT(this->isMutable());
Q_ASSERT(b < e);
Q_ASSERT(b >= this->begin() && b < this->end());
Q_ASSERT(e > this->begin() && e <= this->end());
// Qt5 QList in erase: try to move less data around
// Now:
const T *begin = this->begin();
const T *end = this->end();
if (b - begin < end - e) {
Base::erase(GrowsBackwardsTag{}, b, e);
} else {
Base::erase(GrowsForwardTag{}, b, e);
}
}
};
} // namespace QtPrivate

View File

@ -2,6 +2,7 @@
**
** Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Marc Mutz <marc.mutz@kdab.com>
** Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Giuseppe D'Angelo <giuseppe.dangelo@kdab.com>
** Copyright (C) 2020 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
@ -147,6 +148,9 @@ template <typename Iterator>
using IfAssociativeIteratorHasFirstAndSecond =
typename std::enable_if<AssociativeIteratorHasFirstAndSecond<Iterator>::value, bool>::type;
template <typename T, typename U>
using IfIsNotSame =
typename std::enable_if<!std::is_same<T, U>::value, bool>::type;
} // namespace QtPrivate
QT_END_NAMESPACE

View File

@ -206,8 +206,7 @@ public:
if (d->needsDetach()
|| capacity() - size() < size_t(last - first)
|| shouldGrow) {
// QTBUG-84320: change to GrowsBackwards once supported in operations
auto flags = d->detachFlags() | Data::GrowsForward;
const auto flags = d->detachFlags() | Data::GrowsBackwards;
SimpleVector detached(DataPointer::allocateGrow(d, d->detachCapacity(newSize), newSize,
flags));
@ -273,8 +272,11 @@ public:
if (d->needsDetach()
|| capacity() - size() < size_t(last - first)
|| shouldGrow) {
auto flags = d->detachFlags() | Data::GrowsForward;
if (size_t(position) <= (size() + (last - first)) / 4)
flags |= Data::GrowsBackwards;
SimpleVector detached(DataPointer::allocateGrow(d, d->detachCapacity(newSize), newSize,
d->detachFlags() | Data::GrowsForward));
flags));
if (position)
detached.d->copyAppend(begin, where);

View File

@ -861,8 +861,11 @@ void tst_QArrayData::arrayOps()
QVERIFY(vs[i].isSharedWith(referenceString));
QCOMPARE(vo[i].id, referenceObject.id);
QCOMPARE(int(vo[i].flags), CountedObject::CopyConstructed
| CountedObject::DefaultConstructed);
// A temporary object is created as DefaultConstructed |
// CopyConstructed, then it is used instead of the original value to
// construct elements in the container which are CopyConstructed only
QCOMPARE(int(vo[i].flags), CountedObject::CopyConstructed);
}
////////////////////////////////////////////////////////////////////////////
@ -912,8 +915,12 @@ void tst_QArrayData::arrayOps()
QVERIFY(vs[i].isSharedWith(stringArray[i % 5]));
QCOMPARE(vo[i].id, objArray[i % 5].id);
// Insertion at begin (prepend) caused the elements to move, meaning
// that instead of being displaced, newly added elements got constructed
// in uninitialized memory with DefaultConstructed | CopyConstructed
QCOMPARE(int(vo[i].flags), CountedObject::DefaultConstructed
| CountedObject::CopyAssigned);
| CountedObject::CopyConstructed);
}
for (int i = 5; i < 15; ++i) {
@ -1083,8 +1090,12 @@ void tst_QArrayData::arrayOps2()
QVERIFY(vs[i].isNull());
QCOMPARE(vo[i].id, i);
// Erasing closer to begin causes smaller region [begin, begin + 2) to
// be moved. Thus, to-be-erased region gets reassigned with the elements
// at the beginning
QCOMPARE(int(vo[i].flags), CountedObject::DefaultConstructed
| CountedObject::CopyConstructed);
| CountedObject::CopyConstructed | CountedObject::CopyAssigned);
}
for (size_t i = 2; i < 4; ++i) {
@ -1092,8 +1103,9 @@ void tst_QArrayData::arrayOps2()
QVERIFY(vs[i].isNull());
QCOMPARE(vo[i].id, i + 8);
QCOMPARE(int(vo[i].flags), int(CountedObject::DefaultConstructed)
| CountedObject::CopyAssigned);
// Erasing closer to begin does not affect [begin + 2, begin + 4) region
QCOMPARE(int(vo[i].flags), CountedObject::DefaultConstructed);
}
for (size_t i = 4; i < 7; ++i) {
@ -1101,8 +1113,9 @@ void tst_QArrayData::arrayOps2()
QCOMPARE(vs[i], QString::number(i + 3));
QCOMPARE(vo[i].id, i + 3);
QCOMPARE(int(vo[i].flags), CountedObject::DefaultConstructed
| CountedObject::CopyAssigned);
// Erasing closer to begin does not affect [begin + 2, begin + 4) region
QCOMPARE(int(vo[i].flags), CountedObject::DefaultConstructed);
}
}
@ -1134,15 +1147,18 @@ void tst_QArrayData::arrayOpsExtra()
const auto setupDataPointers = [&allocationOptions] (size_t capacity, size_t initialSize = 0) {
const qsizetype alloc = qsizetype(capacity);
// QTBUG-84320: change to growing function once supported
QArrayDataPointer<int> i(QTypedArrayData<int>::allocate(alloc, allocationOptions));
QArrayDataPointer<QString> s(QTypedArrayData<QString>::allocate(alloc, allocationOptions));
QArrayDataPointer<CountedObject> o(QTypedArrayData<CountedObject>::allocate(alloc, allocationOptions));
auto i = QArrayDataPointer<int>::allocateGrow(QArrayDataPointer<int>(), alloc,
initialSize, allocationOptions);
auto s = QArrayDataPointer<QString>::allocateGrow(QArrayDataPointer<QString>(), alloc,
initialSize, allocationOptions);
auto o = QArrayDataPointer<CountedObject>::allocateGrow(QArrayDataPointer<CountedObject>(), alloc,
initialSize, allocationOptions);
if (initialSize) {
i->appendInitialize(initialSize);
s->appendInitialize(initialSize);
o->appendInitialize(initialSize);
}
// assign unique values
std::generate(i.begin(), i.end(), [] () { static int i = 0; return i++; });
std::generate(s.begin(), s.end(), [] () { static int i = 0; return QString::number(i++); });
@ -1209,11 +1225,6 @@ void tst_QArrayData::arrayOpsExtra()
RUN_TEST_FUNC(testCopyAppend, strData, stringArray.begin(), stringArray.end());
RUN_TEST_FUNC(testCopyAppend, objData, objArray.begin(), objArray.end());
// from self
RUN_TEST_FUNC(testCopyAppend, intData, intData.begin() + 3, intData.begin() + 5);
RUN_TEST_FUNC(testCopyAppend, strData, strData.begin() + 3, strData.begin() + 5);
RUN_TEST_FUNC(testCopyAppend, objData, objData.begin() + 3, objData.begin() + 5);
// append to full
const size_t intDataFreeSpace = intData.constAllocatedCapacity() - intData.size;
QVERIFY(intDataFreeSpace > 0);
@ -1232,6 +1243,59 @@ void tst_QArrayData::arrayOpsExtra()
QCOMPARE(size_t(objData.size), objData.constAllocatedCapacity());
}
// copyAppend (iterator version) - special case of copying from self iterators
{
CountedObject::LeakChecker localLeakChecker; Q_UNUSED(localLeakChecker);
const auto testCopyAppendSelf = [&] (auto &dataPointer, auto first, auto last) {
const size_t originalSize = dataPointer.size;
auto copy = cloneArrayDataPointer(dataPointer, dataPointer.size);
const size_t distance = std::distance(first, last);
auto firstCopy = copy->begin() + std::distance(dataPointer->begin(), first);
dataPointer->copyAppend(first, last);
QCOMPARE(size_t(dataPointer.size), originalSize + distance);
size_t i = 0;
for (; i < originalSize; ++i)
QCOMPARE(dataPointer.data()[i], copy.data()[i]);
for (; i < size_t(dataPointer.size); ++i)
QCOMPARE(dataPointer.data()[i], *(firstCopy + (i - originalSize)));
};
auto [intData, strData, objData] = setupDataPointers(inputSize * 2, inputSize / 2);
// make no free space at the end
intData->appendInitialize(intData.size + intData.freeSpaceAtEnd());
strData->appendInitialize(strData.size + strData.freeSpaceAtEnd());
objData->appendInitialize(objData.size + objData.freeSpaceAtEnd());
// make all values unique. this would ensure that we do not have erroneously passed test
int i = 0;
std::generate(intData.begin(), intData.end(), [&i] () { return i++; });
std::generate(strData.begin(), strData.end(), [&i] () { return QString::number(i++); });
std::generate(objData.begin(), objData.end(), [] () { return CountedObject(); });
// sanity checks:
if (allocationOptions & QArrayData::GrowsBackwards) {
QVERIFY(intData.freeSpaceAtBegin() > 0);
QVERIFY(strData.freeSpaceAtBegin() > 0);
QVERIFY(objData.freeSpaceAtBegin() > 0);
}
QVERIFY(intData.freeSpaceAtBegin() <= intData.size);
QVERIFY(strData.freeSpaceAtBegin() <= strData.size);
QVERIFY(objData.freeSpaceAtBegin() <= objData.size);
QVERIFY(intData.freeSpaceAtEnd() == 0);
QVERIFY(strData.freeSpaceAtEnd() == 0);
QVERIFY(objData.freeSpaceAtEnd() == 0);
// now, append to full size causing the data to move internally. passed
// iterators that refer to the object itself must be used correctly
RUN_TEST_FUNC(testCopyAppendSelf, intData, intData.begin(),
intData.begin() + intData.freeSpaceAtBegin());
RUN_TEST_FUNC(testCopyAppendSelf, strData, strData.begin(),
strData.begin() + strData.freeSpaceAtBegin());
RUN_TEST_FUNC(testCopyAppendSelf, objData, objData.begin(),
objData.begin() + objData.freeSpaceAtBegin());
}
// copyAppend (value version)
{
CountedObject::LeakChecker localLeakChecker; Q_UNUSED(localLeakChecker);
@ -1279,6 +1343,52 @@ void tst_QArrayData::arrayOpsExtra()
QCOMPARE(size_t(objData.size), objData.constAllocatedCapacity());
}
// copyAppend (value version) - special case of copying self value
{
CountedObject::LeakChecker localLeakChecker; Q_UNUSED(localLeakChecker);
const auto testCopyAppendSelf = [&] (auto &dataPointer, size_t n, const auto &value) {
const size_t originalSize = dataPointer.size;
auto copy = cloneArrayDataPointer(dataPointer, dataPointer.size);
auto valueCopy = value;
dataPointer->copyAppend(n, value);
QCOMPARE(size_t(dataPointer.size), originalSize + n);
size_t i = 0;
for (; i < originalSize; ++i)
QCOMPARE(dataPointer.data()[i], copy.data()[i]);
for (; i < size_t(dataPointer.size); ++i)
QCOMPARE(dataPointer.data()[i], valueCopy);
};
auto [intData, strData, objData] = setupDataPointers(inputSize * 2, inputSize / 2);
// make no free space at the end
intData->appendInitialize(intData.size + intData.freeSpaceAtEnd());
strData->appendInitialize(strData.size + strData.freeSpaceAtEnd());
objData->appendInitialize(objData.size + objData.freeSpaceAtEnd());
// make all values unique. this would ensure that we do not have erroneously passed test
int i = 0;
std::generate(intData.begin(), intData.end(), [&i] () { return i++; });
std::generate(strData.begin(), strData.end(), [&i] () { return QString::number(i++); });
std::generate(objData.begin(), objData.end(), [] () { return CountedObject(); });
// sanity checks:
if (allocationOptions & QArrayData::GrowsBackwards) {
QVERIFY(intData.freeSpaceAtBegin() > 0);
QVERIFY(strData.freeSpaceAtBegin() > 0);
QVERIFY(objData.freeSpaceAtBegin() > 0);
}
QVERIFY(intData.freeSpaceAtEnd() == 0);
QVERIFY(strData.freeSpaceAtEnd() == 0);
QVERIFY(objData.freeSpaceAtEnd() == 0);
// now, append to full size causing the data to move internally. passed
// value that refers to the object itself must be used correctly
RUN_TEST_FUNC(testCopyAppendSelf, intData, intData.freeSpaceAtBegin(), intData.data()[0]);
RUN_TEST_FUNC(testCopyAppendSelf, strData, strData.freeSpaceAtBegin(), strData.data()[0]);
RUN_TEST_FUNC(testCopyAppendSelf, objData, objData.freeSpaceAtBegin(), objData.data()[0]);
}
// moveAppend
{
CountedObject::LeakChecker localLeakChecker; Q_UNUSED(localLeakChecker);
@ -1327,6 +1437,63 @@ void tst_QArrayData::arrayOpsExtra()
QCOMPARE(size_t(objData.size), objData.constAllocatedCapacity());
}
// moveAppend - special case of moving from self (this is legal yet rather useless)
{
CountedObject::LeakChecker localLeakChecker; Q_UNUSED(localLeakChecker);
const auto testMoveAppendSelf = [&] (auto &dataPointer, auto first, auto last) {
const size_t originalSize = dataPointer.size;
auto copy = cloneArrayDataPointer(dataPointer, dataPointer.size);
const size_t addedSize = std::distance(first, last);
const size_t firstPos = std::distance(dataPointer->begin(), first);
auto firstCopy = copy->begin() + firstPos;
dataPointer->moveAppend(first, last);
QCOMPARE(size_t(dataPointer.size), originalSize + addedSize);
size_t i = 0;
for (; i < originalSize; ++i) {
if (i >= firstPos && i < (firstPos + addedSize)) // skip "moved from" chunk
continue;
QCOMPARE(dataPointer.data()[i], copy.data()[i]);
}
for (; i < size_t(dataPointer.size); ++i)
QCOMPARE(dataPointer.data()[i], *(firstCopy + (i - originalSize)));
};
auto [intData, strData, objData] = setupDataPointers(inputSize * 2, inputSize / 2);
// make no free space at the end
intData->appendInitialize(intData.size + intData.freeSpaceAtEnd());
strData->appendInitialize(strData.size + strData.freeSpaceAtEnd());
objData->appendInitialize(objData.size + objData.freeSpaceAtEnd());
// make all values unique. this would ensure that we do not have erroneously passed test
int i = 0;
std::generate(intData.begin(), intData.end(), [&i] () { return i++; });
std::generate(strData.begin(), strData.end(), [&i] () { return QString::number(i++); });
std::generate(objData.begin(), objData.end(), [] () { return CountedObject(); });
// sanity checks:
if (allocationOptions & QArrayData::GrowsBackwards) {
QVERIFY(intData.freeSpaceAtBegin() > 0);
QVERIFY(strData.freeSpaceAtBegin() > 0);
QVERIFY(objData.freeSpaceAtBegin() > 0);
}
QVERIFY(intData.freeSpaceAtBegin() <= intData.size);
QVERIFY(strData.freeSpaceAtBegin() <= strData.size);
QVERIFY(objData.freeSpaceAtBegin() <= objData.size);
QVERIFY(intData.freeSpaceAtEnd() == 0);
QVERIFY(strData.freeSpaceAtEnd() == 0);
QVERIFY(objData.freeSpaceAtEnd() == 0);
// now, append to full size causing the data to move internally. passed
// iterators that refer to the object itself must be used correctly
RUN_TEST_FUNC(testMoveAppendSelf, intData, intData.begin(),
intData.begin() + intData.freeSpaceAtBegin());
RUN_TEST_FUNC(testMoveAppendSelf, strData, strData.begin(),
strData.begin() + strData.freeSpaceAtBegin());
RUN_TEST_FUNC(testMoveAppendSelf, objData, objData.begin(),
objData.begin() + objData.freeSpaceAtBegin());
}
// truncate
{
CountedObject::LeakChecker localLeakChecker; Q_UNUSED(localLeakChecker);
@ -1434,6 +1601,55 @@ void tst_QArrayData::arrayOpsExtra()
RUN_TEST_FUNC(testInsertValue, objData, objData.size, 1, CountedObject());
}
// insert - special case of inserting from self value. this test only makes
// sense for prepend - insert at begin.
{
const auto testInsertValueSelf = [&] (auto &dataPointer, size_t n, const auto &value) {
const size_t originalSize = dataPointer.size;
auto copy = cloneArrayDataPointer(dataPointer, dataPointer.size);
auto valueCopy = value;
dataPointer->insert(dataPointer.begin(), n, value);
QCOMPARE(size_t(dataPointer.size), originalSize + n);
size_t i = 0;
for (; i < n; ++i)
QCOMPARE(dataPointer.data()[i], valueCopy);
for (; i < size_t(dataPointer.size); ++i)
QCOMPARE(dataPointer.data()[i], copy.data()[i - n]);
};
CountedObject::LeakChecker localLeakChecker; Q_UNUSED(localLeakChecker);
auto [intData, strData, objData] = setupDataPointers(inputSize * 2, inputSize / 2);
// make no free space at the begin
intData->insert(intData.begin(), intData.freeSpaceAtBegin(), intData.data()[0]);
strData->insert(strData.begin(), strData.freeSpaceAtBegin(), strData.data()[0]);
objData->insert(objData.begin(), objData.freeSpaceAtBegin(), objData.data()[0]);
// make all values unique. this would ensure that we do not have erroneously passed test
int i = 0;
std::generate(intData.begin(), intData.end(), [&i] () { return i++; });
std::generate(strData.begin(), strData.end(), [&i] () { return QString::number(i++); });
std::generate(objData.begin(), objData.end(), [] () { return CountedObject(); });
// sanity checks:
QVERIFY(intData.freeSpaceAtEnd() > 0);
QVERIFY(strData.freeSpaceAtEnd() > 0);
QVERIFY(objData.freeSpaceAtEnd() > 0);
QVERIFY(intData.freeSpaceAtBegin() == 0);
QVERIFY(strData.freeSpaceAtBegin() == 0);
QVERIFY(objData.freeSpaceAtBegin() == 0);
// now, prepend to full size causing the data to move internally. passed
// value that refers to the object itself must be used correctly
RUN_TEST_FUNC(testInsertValueSelf, intData, intData.freeSpaceAtEnd(),
intData.data()[intData.size - 1]);
RUN_TEST_FUNC(testInsertValueSelf, strData, strData.freeSpaceAtEnd(),
strData.data()[strData.size - 1]);
RUN_TEST_FUNC(testInsertValueSelf, objData, objData.freeSpaceAtEnd(),
objData.data()[objData.size - 1]);
}
// emplace
{
CountedObject::LeakChecker localLeakChecker; Q_UNUSED(localLeakChecker);
@ -2429,6 +2645,29 @@ void tst_QArrayData::exceptionSafetyPrimitives_destructor()
QVERIFY(throwingTypeWatcher().destroyedIds[0] == 42);
}
}
// extra: special case of freezing the position
{
auto data = createDataPointer<ThrowingType>(20, 10);
auto reference = createDataPointer<ThrowingType>(20, 10);
reference->erase(reference.end() - 1, reference.end());
data.data()[data.size - 1] = ThrowingType(42);
WatcherScope scope; Q_UNUSED(scope);
{
auto where = data.end();
Destructor destroyer(where);
for (int i = 0; i < 3; ++i) {
--where;
destroyer.freeze();
}
}
--data.size; // destroyed 1 element above
for (qsizetype i = 0; i < data.size; ++i)
QCOMPARE(data.data()[i], reference.data()[i]);
QVERIFY(throwingTypeWatcher().destroyedIds.size() == 1);
QCOMPARE(throwingTypeWatcher().destroyedIds[0], 42);
}
}
void tst_QArrayData::exceptionSafetyPrimitives_mover()