From 56f1208f9e6b9a0fa7a121ed46883e62eaf57088 Mon Sep 17 00:00:00 2001 From: Andrei Golubev Date: Thu, 9 Jul 2020 17:53:48 +0300 Subject: [PATCH] Separate exception safety primitives from operations Refactored certain bits of qarraydataops.h: picked exception-related building blocks and put them into one place, (somewhat) documented the usage, added tests Personally, the existing code seemed rather complicated to analyze (and do mental experiments for corner cases), especially when staring at the whole thing for a while or "returning back" from some other work and I still have my doubts that everything works correctly. Testing the building blocks that are used should: a) increase trust into existing code (provided the usage is correct) b) give more use cases of how to use the building blocks, which in turn would allow to compare and contrast tests vs implementation Task-number: QTBUG-84320 Change-Id: I313a1d1817577507fe07a5b9b7d2c90b0969b490 Reviewed-by: Thiago Macieira --- src/corelib/tools/qarraydataops.h | 384 ++++++----- .../corelib/tools/qarraydata/CMakeLists.txt | 1 + .../corelib/tools/qarraydata/qarraydata.pro | 2 +- .../tools/qarraydata/tst_qarraydata.cpp | 612 +++++++++++++++++- 4 files changed, 802 insertions(+), 197 deletions(-) diff --git a/src/corelib/tools/qarraydataops.h b/src/corelib/tools/qarraydataops.h index bc5f3ac52a..369a8ae574 100644 --- a/src/corelib/tools/qarraydataops.h +++ b/src/corelib/tools/qarraydataops.h @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2020 The Qt Company Ltd. ** Copyright (C) 2016 Intel Corporation. ** Contact: https://www.qt.io/licensing/ ** @@ -46,6 +46,8 @@ #include #include +#include +#include QT_BEGIN_NAMESPACE @@ -53,6 +55,166 @@ template struct QArrayDataPointer; namespace QtPrivate { +/*! + \internal + + This class provides basic building blocks to do reversible operations. This + in turn allows to reason about exception safety under certain conditions. + + This class is not part of the Qt API. It exists for the convenience of other + Qt classes. This class may change from version to version without notice, + or even be removed. + + We mean it. + */ +template +struct QArrayExceptionSafetyPrimitives +{ + using parameter_type = typename QArrayDataPointer::parameter_type; + using iterator = typename QArrayDataPointer::iterator; + + // Constructs a range of elements at the specified position. If an exception + // is thrown during construction, already constructed elements are + // destroyed. By design, only one function (create/copy/clone/move) and only + // once is supposed to be called per class instance. + struct Constructor + { + T *const where; + size_t n = 0; + + template + using iterator_copy_value = decltype(*std::declval()); + template + using iterator_move_value = decltype(std::move(*std::declval())); + + Constructor(T *w) noexcept : where(w) {} + qsizetype create(size_t size) noexcept(std::is_nothrow_default_constructible_v) + { + n = 0; + while (n != size) { + new (where + n) T; + ++n; + } + return qsizetype(std::exchange(n, 0)); + } + template + qsizetype copy(ForwardIt first, ForwardIt last) + noexcept(std::is_nothrow_constructible_v>) + { + n = 0; + for (; first != last; ++first) { + new (where + n) T(*first); + ++n; + } + return qsizetype(std::exchange(n, 0)); + } + qsizetype clone(size_t size, parameter_type t) + noexcept(std::is_nothrow_constructible_v) + { + n = 0; + while (n != size) { + new (where + n) T(t); + ++n; + } + return qsizetype(std::exchange(n, 0)); + } + template + qsizetype move(ForwardIt first, ForwardIt last) + noexcept(std::is_nothrow_constructible_v>) + { + n = 0; + for (; first != last; ++first) { + new (where + n) T(std::move(*first)); + ++n; + } + return qsizetype(std::exchange(n, 0)); + } + ~Constructor() noexcept(std::is_nothrow_destructible_v) + { + while (n) + where[--n].~T(); + } + }; + + // Watches passed iterator. Unless commit() is called, all the elements that + // the watched iterator passes through are deleted at the end of object + // lifetime. + // + // Requirements: when not at starting position, the iterator is expected to + // point to a valid object (to initialized memory) + template + struct Destructor + { + It *iter; + It end; + + Destructor(It &it) noexcept(std::is_nothrow_copy_constructible_v) + : iter(std::addressof(it)), end(it) + { } + void commit() noexcept + { + iter = std::addressof(end); + } + ~Destructor() noexcept(std::is_nothrow_destructible_v) + { + // Step is either 1 or -1 and depends on the interposition of *iter + // and end. Note that *iter is expected to point to a valid object + // (see the logic below). + for (const int step = *iter < end ? 1 : -1; *iter != end; std::advance(*iter, step)) + (*iter)->~T(); + } + }; + + // Moves the data range in memory by the specified amount. Unless commit() + // is called, the data is moved back to the original place at the end of + // object lifetime. + struct Displacer + { + T *const begin; + T *const end; + qsizetype displace; + + static_assert(QTypeInfoQuery::isRelocatable, "Type must be relocatable"); + + Displacer(T *start, T *finish, qsizetype diff) noexcept + : begin(start), end(finish), displace(diff) + { + ::memmove(static_cast(begin + displace), static_cast(begin), + (end - begin) * sizeof(T)); + } + void commit() noexcept { displace = 0; } + ~Displacer() noexcept + { + if (displace) + ::memmove(static_cast(begin), static_cast(begin + displace), + (end - begin) * sizeof(T)); + } + }; + + // Watches passed iterator. Moves the data range (defined as a start + // iterator and a length) to the new starting position at the end of object + // lifetime. + struct Mover + { + T *&destination; + const T *const source; + size_t n; + qsizetype &size; + + static_assert(QTypeInfoQuery::isRelocatable, "Type must be relocatable"); + + Mover(T *&start, size_t length, qsizetype &sz) noexcept + : destination(start), source(start), n(length), size(sz) + { } + ~Mover() noexcept + { + ::memmove(static_cast(destination), static_cast(source), + n * sizeof(T)); + size -= source > destination ? source - destination : destination - source; + } + }; +}; + QT_WARNING_PUSH #if defined(Q_CC_GNU) && Q_CC_GNU >= 700 QT_WARNING_DISABLE_GCC("-Wstringop-overflow") @@ -265,35 +427,13 @@ struct QGenericArrayOps Q_ASSERT(b <= e); Q_ASSERT(size_t(e - b) <= this->allocatedCapacity() - this->size); + typedef typename QArrayExceptionSafetyPrimitives::Constructor CopyConstructor; + // Provides strong exception safety guarantee, // provided T::~T() nothrow - struct CopyConstructor - { - CopyConstructor(T *w) : where(w) {} - - void copy(T *src, const T *const srcEnd) - { - n = 0; - for (; src != srcEnd; ++src) { - new (where + n) T(std::move(*src)); - ++n; - } - n = 0; - } - - ~CopyConstructor() - { - while (n) - where[--n].~T(); - } - - T *const where; - size_t n; - } copier(this->end()); - - copier.copy(b, e); - this->size += (e - b); + CopyConstructor copier(this->end()); + this->size += copier.move(b, e); } void copyAppend(size_t n, parameter_type t) @@ -341,6 +481,8 @@ struct QGenericArrayOps Q_ASSERT(e <= where || b > this->end() || where == this->end()); // No overlap or append Q_ASSERT(size_t(e - b) <= this->allocatedCapacity() - this->size); + typedef typename QArrayExceptionSafetyPrimitives::template Destructor Destructor; + // Array may be truncated at where in case of exceptions T *const end = this->end(); @@ -349,28 +491,7 @@ struct QGenericArrayOps const T *const step1End = where + qMax(e - b, end - where); - struct Destructor - { - Destructor(T *&it) - : iter(&it) - , end(it) - { - } - - void commit() - { - iter = &end; - } - - ~Destructor() - { - for (; *iter != end; --*iter) - (*iter)->~T(); - } - - T **iter; - T *end; - } destroyer(writeIter); + Destructor destroyer(writeIter); // Construct new elements in array while (writeIter != step1End) { @@ -404,6 +525,8 @@ struct QGenericArrayOps Q_ASSERT(where >= this->begin() && where <= this->end()); Q_ASSERT(this->allocatedCapacity() - this->size >= n); + typedef typename QArrayExceptionSafetyPrimitives::template Destructor Destructor; + // Array may be truncated at where in case of exceptions T *const end = this->end(); const T *readIter = end; @@ -411,28 +534,7 @@ struct QGenericArrayOps const T *const step1End = where + qMax(n, end - where); - struct Destructor - { - Destructor(T *&it) - : iter(&it) - , end(it) - { - } - - void commit() - { - iter = &end; - } - - ~Destructor() - { - for (; *iter != end; --*iter) - (*iter)->~T(); - } - - T **iter; - T *end; - } destroyer(writeIter); + Destructor destroyer(writeIter); // Construct new elements in array while (writeIter != step1End) { @@ -542,62 +644,17 @@ struct QMovableArrayOps Q_ASSERT(e <= where || b > this->end() || where == this->end()); // No overlap or append Q_ASSERT(size_t(e - b) <= this->allocatedCapacity() - this->size); + typedef typename QArrayExceptionSafetyPrimitives::Displacer ReversibleDisplace; + typedef typename QArrayExceptionSafetyPrimitives::Constructor CopyConstructor; + // Provides strong exception safety guarantee, // provided T::~T() nothrow - struct ReversibleDisplace - { - ReversibleDisplace(T *start, T *finish, size_t diff) - : begin(start) - , end(finish) - , displace(diff) - { - ::memmove(static_cast(begin + displace), static_cast(begin), - (end - begin) * sizeof(T)); - } - - void commit() { displace = 0; } - - ~ReversibleDisplace() - { - if (displace) - ::memmove(static_cast(begin), static_cast(begin + displace), - (end - begin) * sizeof(T)); - } - - T *const begin; - T *const end; - size_t displace; - - } displace(where, this->end(), size_t(e - b)); - - struct CopyConstructor - { - CopyConstructor(T *w) : where(w) {} - - void copy(const T *src, const T *const srcEnd) - { - n = 0; - for (; src != srcEnd; ++src) { - new (where + n) T(*src); - ++n; - } - n = 0; - } - - ~CopyConstructor() - { - while (n) - where[--n].~T(); - } - - T *const where; - size_t n; - } copier(where); - - copier.copy(b, e); + ReversibleDisplace displace(where, this->end(), e - b); + CopyConstructor copier(where); + const auto copiedSize = copier.copy(b, e); displace.commit(); - this->size += (e - b); + this->size += copiedSize; } void insert(T *where, size_t n, parameter_type t) @@ -606,62 +663,17 @@ struct QMovableArrayOps Q_ASSERT(where >= this->begin() && where <= this->end()); Q_ASSERT(this->allocatedCapacity() - this->size >= n); + typedef typename QArrayExceptionSafetyPrimitives::Displacer ReversibleDisplace; + typedef typename QArrayExceptionSafetyPrimitives::Constructor CopyConstructor; + // Provides strong exception safety guarantee, // provided T::~T() nothrow - struct ReversibleDisplace - { - ReversibleDisplace(T *start, T *finish, size_t diff) - : begin(start) - , end(finish) - , displace(diff) - { - ::memmove(static_cast(begin + displace), static_cast(begin), - (end - begin) * sizeof(T)); - } - - void commit() { displace = 0; } - - ~ReversibleDisplace() - { - if (displace) - ::memmove(static_cast(begin), static_cast(begin + displace), - (end - begin) * sizeof(T)); - } - - T *const begin; - T *const end; - size_t displace; - - } displace(where, this->end(), n); - - struct CopyConstructor - { - CopyConstructor(T *w) : where(w) {} - - void copy(size_t count, parameter_type proto) - { - n = 0; - while (count--) { - new (where + n) T(proto); - ++n; - } - n = 0; - } - - ~CopyConstructor() - { - while (n) - where[--n].~T(); - } - - T *const where; - size_t n; - } copier(where); - - copier.copy(n, t); + ReversibleDisplace displace(where, this->end(), qsizetype(n)); + CopyConstructor copier(where); + const auto copiedSize = copier.clone(n, t); displace.commit(); - this->size += int(n); + this->size += copiedSize; } // use moving insert @@ -674,27 +686,9 @@ struct QMovableArrayOps Q_ASSERT(b >= this->begin() && b < this->end()); Q_ASSERT(e > this->begin() && e <= this->end()); - struct Mover - { - Mover(T *&start, const T *finish, qsizetype &sz) - : destination(start) - , source(start) - , n(finish - start) - , size(sz) - { - } + typedef typename QArrayExceptionSafetyPrimitives::Mover Mover; - ~Mover() - { - ::memmove(static_cast(destination), static_cast(source), n * sizeof(T)); - size -= (source - destination); - } - - T *&destination; - const T *const source; - size_t n; - qsizetype &size; - } mover(e, this->end(), this->size); + Mover mover(e, static_cast(this->end()) - e, this->size); // destroy the elements we're erasing do { diff --git a/tests/auto/corelib/tools/qarraydata/CMakeLists.txt b/tests/auto/corelib/tools/qarraydata/CMakeLists.txt index 94c97b8327..fac007f3b5 100644 --- a/tests/auto/corelib/tools/qarraydata/CMakeLists.txt +++ b/tests/auto/corelib/tools/qarraydata/CMakeLists.txt @@ -5,6 +5,7 @@ ##################################################################### qt_add_test(tst_qarraydata + EXCEPTIONS SOURCES simplevector.h tst_qarraydata.cpp diff --git a/tests/auto/corelib/tools/qarraydata/qarraydata.pro b/tests/auto/corelib/tools/qarraydata/qarraydata.pro index ee3faa9ad7..36708dfd59 100644 --- a/tests/auto/corelib/tools/qarraydata/qarraydata.pro +++ b/tests/auto/corelib/tools/qarraydata/qarraydata.pro @@ -2,4 +2,4 @@ TARGET = tst_qarraydata SOURCES += $$PWD/tst_qarraydata.cpp HEADERS += $$PWD/simplevector.h QT = core testlib -CONFIG += testcase +CONFIG += testcase exceptions diff --git a/tests/auto/corelib/tools/qarraydata/tst_qarraydata.cpp b/tests/auto/corelib/tools/qarraydata/tst_qarraydata.cpp index 3f96d7e782..2d17a170f8 100644 --- a/tests/auto/corelib/tools/qarraydata/tst_qarraydata.cpp +++ b/tests/auto/corelib/tools/qarraydata/tst_qarraydata.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2020 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the test suite of the Qt Toolkit. @@ -37,6 +37,7 @@ #include #include #include +#include // A wrapper for a test function. Calls a function, if it fails, reports failure #define RUN_TEST_FUNC(test, ...) \ @@ -75,6 +76,12 @@ private slots: void variadicLiterals(); void rValueReferences(); void grow(); +#ifndef QT_NO_EXCEPTIONS + void exceptionSafetyPrimitives_constructor(); + void exceptionSafetyPrimitives_destructor(); + void exceptionSafetyPrimitives_mover(); + void exceptionSafetyPrimitives_displacer(); +#endif }; template const T &const_(const T &t) { return t; } @@ -1807,5 +1814,608 @@ void tst_QArrayData::grow() } } +#ifndef QT_NO_EXCEPTIONS +struct ThrowingTypeWatcher +{ + std::vector destroyedIds; + bool watch = false; + void destroyed(int id) + { + if (watch) + destroyedIds.push_back(id); + } +}; +ThrowingTypeWatcher& throwingTypeWatcher() { static ThrowingTypeWatcher global; return global; } + +struct ThrowingType +{ + static unsigned int throwOnce; + static constexpr char throwString[] = "Requested to throw"; + void checkThrow() { + // deferred throw + if (throwOnce > 0) { + --throwOnce; + if (throwOnce == 0) { + throw std::runtime_error(throwString); + } + } + return; + } + int id = 0; + + ThrowingType(int val = 0) : id(val) + { + checkThrow(); + } + ThrowingType(const ThrowingType &other) : id(other.id) + { + checkThrow(); + } + ThrowingType& operator=(const ThrowingType &other) + { + id = other.id; + checkThrow(); + return *this; + } + ~ThrowingType() + { + throwingTypeWatcher().destroyed(id); // notify global watcher + id = -1; + } +}; +unsigned int ThrowingType::throwOnce = 0; +bool operator==(const ThrowingType &a, const ThrowingType &b) { + return a.id == b.id; +} +QT_BEGIN_NAMESPACE +Q_DECLARE_TYPEINFO(ThrowingType, Q_RELOCATABLE_TYPE); +QT_END_NAMESPACE + +template // T must be constructible from a single int parameter +static QArrayDataPointer createDataPointer(qsizetype capacity, qsizetype initSize) +{ + QArrayDataPointer adp(QTypedArrayData::allocate(capacity)); + adp->appendInitialize(initSize); + // assign unique values + int i = 0; + std::generate(adp.begin(), adp.end(), [&i] () { return T(i++); }); + return adp; +} + +void tst_QArrayData::exceptionSafetyPrimitives_constructor() +{ + using Prims = QtPrivate::QArrayExceptionSafetyPrimitives; + using Constructor = typename Prims::Constructor; + + struct WatcherScope + { + WatcherScope() { throwingTypeWatcher().watch = true; } + ~WatcherScope() + { + throwingTypeWatcher().watch = false; + throwingTypeWatcher().destroyedIds.clear(); + } + }; + + const auto doConstruction = [] (auto &dataPointer, auto where, auto op) { + Constructor ctor(where); + dataPointer.size += op(ctor); + }; + + // empty ranges + { + auto data = createDataPointer(20, 10); + const auto originalSize = data.size; + const std::array emptyRange{}; + + doConstruction(data, data.end(), [] (Constructor &ctor) { return ctor.create(0); }); + QCOMPARE(data.size, originalSize); + + doConstruction(data, data.end(), [&emptyRange] (Constructor &ctor) { + return ctor.copy(emptyRange.begin(), emptyRange.end()); + }); + QCOMPARE(data.size, originalSize); + + doConstruction(data, data.end(), [] (Constructor &ctor) { + return ctor.clone(0, ThrowingType(42)); + }); + QCOMPARE(data.size, originalSize); + + doConstruction(data, data.end(), [emptyRange] (Constructor &ctor) mutable { + return ctor.move(emptyRange.begin(), emptyRange.end()); + }); + QCOMPARE(data.size, originalSize); + } + + // successful create + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + reference->appendInitialize(reference.size + 1); + + doConstruction(data, data.end(), [] (Constructor &ctor) { return ctor.create(1); }); + + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + } + + // successful copy + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + const std::array source = { + ThrowingType(42), ThrowingType(43), ThrowingType(44) + }; + reference->copyAppend(source.begin(), source.end()); + + doConstruction(data, data.end(), [&source] (Constructor &ctor) { + return ctor.copy(source.begin(), source.end()); + }); + + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + + reference->copyAppend(2, source[0]); + + doConstruction(data, data.end(), [&source] (Constructor &ctor) { + return ctor.clone(2, source[0]); + }); + + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + } + + // successful move + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + const std::array source = { + ThrowingType(42), ThrowingType(43), ThrowingType(44) + }; + reference->copyAppend(source.begin(), source.end()); + + doConstruction(data, data.end(), [source] (Constructor &ctor) mutable { + return ctor.move(source.begin(), source.end()); + }); + + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + } + + // failed create + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + + for (uint throwOnNthConstruction : {1, 3}) { + WatcherScope scope; Q_UNUSED(scope); + try { + ThrowingType::throwOnce = throwOnNthConstruction; + doConstruction(data, data.end(), [] (Constructor &ctor) { + return ctor.create(5); + }); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + QCOMPARE(throwingTypeWatcher().destroyedIds.size(), (throwOnNthConstruction - 1)); + for (auto id : throwingTypeWatcher().destroyedIds) + QCOMPARE(id, 0); + } + } + } + + // failed copy + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + const std::array source = { + ThrowingType(42), ThrowingType(43), ThrowingType(44), ThrowingType(170) + }; + + // copy range + for (uint throwOnNthConstruction : {1, 3}) { + WatcherScope scope; Q_UNUSED(scope); + try { + ThrowingType::throwOnce = throwOnNthConstruction; + doConstruction(data, data.end(), [&source] (Constructor &ctor) { + return ctor.copy(source.begin(), source.end()); + }); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + const auto destroyedSize = throwingTypeWatcher().destroyedIds.size(); + QCOMPARE(destroyedSize, (throwOnNthConstruction - 1)); + for (size_t i = 0; i < destroyedSize; ++i) + QCOMPARE(throwingTypeWatcher().destroyedIds[i], source[destroyedSize - i - 1]); + } + } + + // copy value + for (uint throwOnNthConstruction : {1, 3}) { + const ThrowingType value(512); + QVERIFY(QArrayDataPointer::pass_parameter_by_value == false); + WatcherScope scope; Q_UNUSED(scope); + try { + ThrowingType::throwOnce = throwOnNthConstruction; + doConstruction(data, data.end(), [&source, &value] (Constructor &ctor) { + return ctor.clone(5, value); + }); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + QCOMPARE(throwingTypeWatcher().destroyedIds.size(), (throwOnNthConstruction - 1)); + for (auto id : throwingTypeWatcher().destroyedIds) + QCOMPARE(id, 512); + } + } + } + + // failed move + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + const std::array source = { + ThrowingType(42), ThrowingType(43), ThrowingType(44), ThrowingType(170) + }; + + for (uint throwOnNthConstruction : {1, 3}) { + WatcherScope scope; Q_UNUSED(scope); + try { + ThrowingType::throwOnce = throwOnNthConstruction; + doConstruction(data, data.end(), [source] (Constructor &ctor) mutable { + return ctor.move(source.begin(), source.end()); + }); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + const auto destroyedSize = throwingTypeWatcher().destroyedIds.size(); + QCOMPARE(destroyedSize, (throwOnNthConstruction - 1)); + for (size_t i = 0; i < destroyedSize; ++i) + QCOMPARE(throwingTypeWatcher().destroyedIds[i], source[destroyedSize - i - 1]); + } + } + } +} + +void tst_QArrayData::exceptionSafetyPrimitives_destructor() +{ + using Prims = QtPrivate::QArrayExceptionSafetyPrimitives; + using Destructor = typename Prims::Destructor<>; + + struct WatcherScope + { + WatcherScope() { throwingTypeWatcher().watch = true; } + ~WatcherScope() + { + throwingTypeWatcher().watch = false; + throwingTypeWatcher().destroyedIds.clear(); + } + }; + + // successful operation with no rollback, elements added from left to right + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + reference->insert(reference.end(), 2, ThrowingType(42)); + + WatcherScope scope; Q_UNUSED(scope); + { + auto where = data.end() - 1; + Destructor destroyer(where); + for (int i = 0; i < 2; ++i) { + new (where + 1) ThrowingType(42); + ++where; + ++data.size; + } + destroyer.commit(); + } + + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + QVERIFY(throwingTypeWatcher().destroyedIds.size() == 0); + } + + // failed operation with rollback, elements added from left to right + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + + WatcherScope scope; Q_UNUSED(scope); + try { + auto where = data.end() - 1; + Destructor destroyer(where); + for (int i = 0; i < 2; ++i) { + new (where + 1) ThrowingType(42 + i); + ++where; + ThrowingType::throwOnce = 1; + } + QFAIL("Unreachable line!"); + destroyer.commit(); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + QCOMPARE(data.size, reference.size); + 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); + } + } + + // successful operation with no rollback, elements added from right to left + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + reference->erase(reference.begin(), reference.begin() + 2); + reference->insert(reference.begin(), 2, ThrowingType(42)); + + data.begin()->~ThrowingType(); + data.begin()->~ThrowingType(); + data.size -= 2; + WatcherScope scope; Q_UNUSED(scope); + { + auto where = data.begin() + 2; // Note: not updated data ptr, so begin + 2 + Destructor destroyer(where); + for (int i = 0; i < 2; ++i) { + new (where - 1) ThrowingType(42); + --where; + ++data.size; + } + destroyer.commit(); + } + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + QVERIFY(throwingTypeWatcher().destroyedIds.size() == 0); + } + + // failed operation with rollback, elements added from right to left + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + reference->erase(reference.begin(), reference.begin() + 2); + + data.begin()->~ThrowingType(); + data.begin()->~ThrowingType(); + data.size -= 2; + WatcherScope scope; Q_UNUSED(scope); + try { + auto where = data.begin() + 2; // Note: not updated data ptr, so begin + 2 + Destructor destroyer(where); + for (int i = 0; i < 2; ++i) { + new (where - 1) ThrowingType(42 + i); + --where; + ThrowingType::throwOnce = 1; + } + QFAIL("Unreachable line!"); + destroyer.commit(); + } catch (const std::runtime_error &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i + 2], reference.data()[i]); + QVERIFY(throwingTypeWatcher().destroyedIds.size() == 1); + QCOMPARE(throwingTypeWatcher().destroyedIds[0], 42); + } + } + + // extra: the very first operation throws - destructor has to do nothing, + // since nothing is properly constructed + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + + WatcherScope scope; Q_UNUSED(scope); + try { + auto where = data.end() - 1; + Destructor destroyer(where); + ThrowingType::throwOnce = 1; + new (where + 1) ThrowingType(42); + ++where; + QFAIL("Unreachable line!"); + destroyer.commit(); + } catch (const std::runtime_error &e) { + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + QVERIFY(throwingTypeWatcher().destroyedIds.size() == 0); + } + } + + // extra: special case when iterator is intentionally out of bounds: this is + // to cover the case when we work on the uninitialized memory region instead + // of being near the border + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + reference->erase(reference.begin(), reference.begin() + 2); + + data.begin()->~ThrowingType(); + data.begin()->~ThrowingType(); + data.size -= 2; + WatcherScope scope; Q_UNUSED(scope); + try { + auto where = data.begin() - 1; // Note: intentionally out of range + Destructor destroyer(where); + for (int i = 0; i < 2; ++i) { + new (where + 1) ThrowingType(42); + ++where; + ThrowingType::throwOnce = 1; + } + QFAIL("Unreachable line!"); + destroyer.commit(); + } catch (const std::runtime_error &e) { + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i + 2], reference.data()[i]); + QVERIFY(throwingTypeWatcher().destroyedIds.size() == 1); + QVERIFY(throwingTypeWatcher().destroyedIds[0] == 42); + } + } +} + +void tst_QArrayData::exceptionSafetyPrimitives_mover() +{ + QVERIFY(QTypeInfo::isRelocatable); + using Prims = QtPrivate::QArrayExceptionSafetyPrimitives; + using Mover = typename Prims::Mover; + + const auto testMoveLeft = [] (size_t posB, size_t posE) { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + + ThrowingType *b = data.begin() + posB; + ThrowingType *e = data.begin() + posE; + const auto originalSize = data.size; + const auto length = std::distance(b, e); + { + Mover mover(e, static_cast(data.end()) - e, data.size); + while (e != b) + (--e)->~ThrowingType(); + } + QCOMPARE(data.size + length, originalSize); + qsizetype i = 0; + for (; i < std::distance(static_cast(data.begin()), b); ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + for (; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i + length]); + }; + + const auto testMoveRight = [] (size_t posB, size_t posE) { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + + ThrowingType *begin = data.begin(); + ThrowingType *b = data.begin() + posB; + ThrowingType *e = data.begin() + posE; + const auto originalSize = data.size; + const auto length = std::distance(b, e); + { + Mover mover(begin, b - static_cast(data.begin()), data.size); + while (b != e) { + ++begin; + (b++)->~ThrowingType(); + } + } + QCOMPARE(data.size + length, originalSize); + + // restore original data size + { + for (qsizetype i = 0; i < length; ++i) { + new (static_cast(data.begin() + i)) ThrowingType(42); + ++data.size; + } + } + + qsizetype i = length; + for (; i < std::distance(static_cast(data.begin()), e); ++i) + QCOMPARE(data.data()[i], reference.data()[i - length]); + for (; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + }; + + // normal move left + RUN_TEST_FUNC(testMoveLeft, 2, 4); + // no move left + RUN_TEST_FUNC(testMoveLeft, 2, 2); + // normal move right + RUN_TEST_FUNC(testMoveRight, 3, 5); + // no move right + RUN_TEST_FUNC(testMoveRight, 4, 4); +} + +void tst_QArrayData::exceptionSafetyPrimitives_displacer() +{ + QVERIFY(QTypeInfo::isRelocatable); + using Prims = QtPrivate::QArrayExceptionSafetyPrimitives; + const auto doDisplace = [] (auto &dataPointer, auto start, auto finish, qsizetype diff) { + typename Prims::Displacer displace(start, finish, diff); + new (start) ThrowingType(42); + ++dataPointer.size; + displace.commit(); + }; + + // successful operation with displace to the right + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + reference->insert(reference.end() - 1, 1, ThrowingType(42)); + + auto where = data.end() - 1; + doDisplace(data, where, data.end(), 1); + + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + } + + // failed operation with displace to the right + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + try { + ThrowingType::throwOnce = 1; + doDisplace(data, data.end() - 1, data.end(), 1); + QFAIL("Unreachable line!"); + } catch (const std::exception &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + } + } + + // successful operation with displace to the left + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + reference.data()[0] = reference.data()[1]; + reference.data()[1] = ThrowingType(42); + + data.begin()->~ThrowingType(); // free space at begin + --data.size; + auto where = data.begin() + 1; + doDisplace(data, where, where + 1, -1); + + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i], reference.data()[i]); + } + + // failed operation with displace to the left + { + auto data = createDataPointer(20, 10); + auto reference = createDataPointer(20, 10); + reference->erase(reference.begin(), reference.begin() + 1); + + try { + data.begin()->~ThrowingType(); // free space at begin + --data.size; + ThrowingType::throwOnce = 1; + auto where = data.begin() + 1; + doDisplace(data, where, where + 1, -1); + QFAIL("Unreachable line!"); + } catch (const std::exception &e) { + QCOMPARE(std::string(e.what()), ThrowingType::throwString); + QCOMPARE(data.size, reference.size); + for (qsizetype i = 0; i < data.size; ++i) + QCOMPARE(data.data()[i + 1], reference.data()[i]); + } + } +} +#endif // QT_NO_EXCEPTIONS + QTEST_APPLESS_MAIN(tst_QArrayData) #include "tst_qarraydata.moc"