diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 31b81734e86..64d590afc74 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -280,6 +280,7 @@ qt_internal_add_module(Core tools/qflatmap_p.h tools/qfreelist.cpp tools/qfreelist_p.h tools/qfunctionaltools_impl.h + tools/quniquehandle_p.h tools/qhashfunctions.h tools/qiterator.h tools/qline.cpp tools/qline.h diff --git a/src/corelib/tools/quniquehandle_p.h b/src/corelib/tools/quniquehandle_p.h new file mode 100644 index 00000000000..7af1536c2ed --- /dev/null +++ b/src/corelib/tools/quniquehandle_p.h @@ -0,0 +1,225 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QUNIQUEHANDLE_P_H +#define QUNIQUEHANDLE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +/*! \internal QUniqueHandle is a general purpose RAII wrapper intended + for interfacing with resource-allocating C-style APIs, for example + operating system APIs, database engine APIs, or any other scenario + where resources are allocated and released, and where pointer + semantics does not seem a perfect fit. + + QUniqueHandle does not support copying, because it is intended to + maintain ownership of resources that can not be copied. This makes + it safer to use than naked handle types, since ownership is + maintained by design. + + The underlying handle object is described using a client supplied + HandleTraits object that is implemented per resource type. The + traits struct must describe two properties of a handle: + + 1) What value is considered invalid + 2) How to close a resource. + + Example 1: + + struct InvalidHandleTraits { + using Type = HANDLE; + + static Type invalidValue() { + return INVALID_HANDLE_VALUE; + } + + static bool close(Type handle) { + return CloseHandle(handle) != 0; + } + } + + using FileHandle = QUniqueHandle; + + Usage: + + // Takes ownership of returned handle. + FileHandle handle{ CreateFile(...) }; + + if (!handle.isValid()) { + qDebug() << GetLastError() + return; + } + + ... + + Example 2: + + struct SqLiteTraits { + using Type = sqlite3*; + + static Type invalidValue() { + return nullptr; + } + + static bool close(Type handle) { + sqlite3_close(handle); + return true; + } + } + + using DbHandle = QUniqueHandle; + + Usage: + + DbHandle h; + + // Take ownership of returned handle. + int result = sqlite3_open(":memory:", &h); + + ... + + NOTE: The QUniqueHandle assumes that closing a resource is + guaranteed to succeed, and provides no support for handling failure + to close a resource. It is therefore only recommended for use cases + where failure to close a resource is either not an error, or an + unrecoverable error. +*/ + +// clang-format off + +template +class QUniqueHandle +{ +public: + using Type = typename HandleTraits::Type; + + QUniqueHandle() = default; + + explicit QUniqueHandle(const Type &handle) noexcept + : m_handle{ handle } + {} + + QUniqueHandle(QUniqueHandle &&other) noexcept + : m_handle{ other.release() } + {} + + ~QUniqueHandle() noexcept + { + close(); + } + + QUniqueHandle& operator=(QUniqueHandle &&rhs) noexcept + { + if (this != std::addressof(rhs)) + reset(rhs.release()); + + return *this; + } + + QUniqueHandle(const QUniqueHandle &) = delete; + QUniqueHandle &operator=(const QUniqueHandle &) = delete; + + + [[nodiscard]] bool isValid() const noexcept + { + return m_handle != HandleTraits::invalidValue(); + } + + [[nodiscard]] explicit operator bool() const noexcept + { + return isValid(); + } + + [[nodiscard]] Type get() const noexcept + { + return m_handle; + } + + void reset(const Type& handle) noexcept + { + if (handle == m_handle) + return; + + close(); + m_handle = handle; + } + + [[nodiscard]] Type release() noexcept + { + Type handle = m_handle; + m_handle = HandleTraits::invalidValue(); + return handle; + } + + [[nodiscard]] Type *operator&() noexcept // NOLINT(google-runtime-operator) + { + Q_ASSERT(!isValid()); + return &m_handle; + } + + void close() noexcept + { + if (!isValid()) + return; + + const bool success = HandleTraits::close(m_handle); + Q_ASSERT(success); + + m_handle = HandleTraits::invalidValue(); + } + + [[nodiscard]] friend bool operator==(const QUniqueHandle &lhs, const QUniqueHandle &rhs) noexcept + { + return lhs.get() == rhs.get(); + } + + [[nodiscard]] friend bool operator!=(const QUniqueHandle &lhs, const QUniqueHandle &rhs) noexcept + { + return lhs.get() != rhs.get(); + } + + [[nodiscard]] friend bool operator<(const QUniqueHandle &lhs, const QUniqueHandle &rhs) noexcept + { + return lhs.get() < rhs.get(); + } + + [[nodiscard]] friend bool operator<=(const QUniqueHandle &lhs, const QUniqueHandle &rhs) noexcept + { + return lhs.get() <= rhs.get(); + } + + [[nodiscard]] friend bool operator>(const QUniqueHandle &lhs, const QUniqueHandle &rhs) noexcept + { + return lhs.get() > rhs.get(); + } + + [[nodiscard]] friend bool operator>=(const QUniqueHandle &lhs, const QUniqueHandle &rhs) noexcept + { + return lhs.get() >= rhs.get(); + } + +private: + Type m_handle{ HandleTraits::invalidValue() }; +}; + +// clang-format on + +QT_END_NAMESPACE + +#endif diff --git a/tests/auto/corelib/tools/CMakeLists.txt b/tests/auto/corelib/tools/CMakeLists.txt index 8eec56758b5..22836aea0e2 100644 --- a/tests/auto/corelib/tools/CMakeLists.txt +++ b/tests/auto/corelib/tools/CMakeLists.txt @@ -48,6 +48,7 @@ add_subdirectory(qsize) add_subdirectory(qsizef) add_subdirectory(qspan) add_subdirectory(qstl) +add_subdirectory(quniquehandle) add_subdirectory(qvarlengtharray) add_subdirectory(qversionnumber) add_subdirectory(qtimeline) diff --git a/tests/auto/corelib/tools/quniquehandle/CMakeLists.txt b/tests/auto/corelib/tools/quniquehandle/CMakeLists.txt new file mode 100644 index 00000000000..fe46826f37d --- /dev/null +++ b/tests/auto/corelib/tools/quniquehandle/CMakeLists.txt @@ -0,0 +1,15 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_quniquehandle LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_quniquehandle + SOURCES + tst_quniquehandle.cpp + LIBRARIES + Qt::CorePrivate +) diff --git a/tests/auto/corelib/tools/quniquehandle/tst_quniquehandle.cpp b/tests/auto/corelib/tools/quniquehandle/tst_quniquehandle.cpp new file mode 100644 index 00000000000..82fb39ece74 --- /dev/null +++ b/tests/auto/corelib/tools/quniquehandle/tst_quniquehandle.cpp @@ -0,0 +1,308 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include + +#include + +QT_USE_NAMESPACE; + +// clang-format off +namespace GlobalResource { + +std::array s_resources = { false, false, false }; + +using handle = size_t; +constexpr handle s_invalidHandle = static_cast(-1); + +handle open() +{ + const auto it = std::find_if(s_resources.begin(), s_resources.end(), + [](bool resource) { + return !resource; + }); + + if (it == s_resources.end()) + return s_invalidHandle; + + *it = true; + + return std::distance(s_resources.begin(), it); +} + +bool open(handle* dest) +{ + const handle resource = open(); + + if (resource == s_invalidHandle) + return false; + + *dest = resource; + return true; +} + +bool close(handle h) +{ + if (h >= s_resources.size()) + return false; // Invalid handle + + if (!s_resources[h]) + return false; // Un-allocated resource + + s_resources[h] = false; + return true; +} + +bool isOpen(handle h) +{ + return s_resources[h]; +} + +void reset() +{ + std::fill(s_resources.begin(), s_resources.end(), false); +} + +bool isReset() +{ + return std::all_of(s_resources.begin(), s_resources.end(), [](bool res) { + return !res; + }); +} + +} // namespace GlobalResource + +struct TestTraits +{ + using Type = GlobalResource::handle; + + static bool close(Type handle) + { + return GlobalResource::close(handle); + } + + static Type invalidValue() noexcept + { + return GlobalResource::s_invalidHandle; + } +}; + +using Handle = QUniqueHandle; + +class tst_QUniqueHandle : public QObject +{ + Q_OBJECT + +private slots: + + void init() const + { + GlobalResource::reset(); + } + + void cleanup() const + { + QVERIFY(GlobalResource::isReset()); + } + + void defaultConstructor_initializesToInvalidHandle() const + { + const Handle h; + QCOMPARE_EQ(h.get(), TestTraits::invalidValue()); + } + + void constructor_initializesToValid_whenCalledWithValidHandle() const + { + const auto res = GlobalResource::open(); + + const Handle h{ res }; + + QCOMPARE_EQ(h.get(), res); + } + + void copyConstructor_and_assignmentOperator_areDeleted() const + { + static_assert(!std::is_copy_constructible_v && !std::is_copy_assignable_v); + } + + void moveConstructor_movesOwnershipAndResetsSource() const + { + Handle source{ GlobalResource::open() }; + const Handle dest{ std::move(source) }; + + QVERIFY(!source.isValid()); + QVERIFY(dest.isValid()); + QVERIFY(GlobalResource::isOpen(dest.get())); + } + + void moveAssignment_movesOwnershipAndResetsSource() const + { + Handle source{ GlobalResource::open() }; + Handle dest; + dest = { std::move(source) }; + + QVERIFY(!source.isValid()); + QVERIFY(dest.isValid()); + QVERIFY(GlobalResource::isOpen(dest.get())); + } + + void isValid_returnsFalse_onlyWhenHandleIsInvalid() const + { + const Handle invalid; + QVERIFY(!invalid.isValid()); + + const Handle valid{ GlobalResource::open() }; + QVERIFY(valid.isValid()); + } + + void destructor_callsClose_whenHandleIsValid() + { + { + const Handle h0{ GlobalResource::open() }; + const Handle h1{ GlobalResource::open() }; + const Handle h2{ GlobalResource::open() }; + QVERIFY(!GlobalResource::isReset()); + } + + QVERIFY(GlobalResource::isReset()); + } + + void operatorBool_returnsFalse_onlyWhenHandleIsInvalid() const + { + const Handle invalid; + QVERIFY(!invalid); + + const Handle valid{ GlobalResource::open() }; + QVERIFY(valid); + } + + void get_returnsValue() const + { + const Handle invalid; + QCOMPARE_EQ(invalid.get(), GlobalResource::s_invalidHandle); + + const auto resource = GlobalResource::open(); + const Handle valid{ resource }; + QCOMPARE_EQ(valid.get(), resource); + } + + void reset_resetsPreviousValueAndTakesOwnership() const + { + const auto resource0 = GlobalResource::open(); + const auto resource1 = GlobalResource::open(); + + Handle h1{ resource0 }; + h1.reset(resource1); + + QVERIFY(!GlobalResource::isOpen(resource0)); + QVERIFY(GlobalResource::isOpen(resource1)); + } + + void release_returnsInvalidResource_whenCalledOnInvalidHandle() const + { + Handle h; + QCOMPARE_EQ(h.release(), GlobalResource::s_invalidHandle); + } + + void release_releasesOwnershipAndReturnsResource_whenHandleOwnsObject() const + { + GlobalResource::handle resource{ GlobalResource::open() }; + GlobalResource::handle released{}; + { + Handle h{ resource }; + released = h.release(); + } + QVERIFY(GlobalResource::isOpen(resource)); + QCOMPARE_EQ(resource, released); + + GlobalResource::close(resource); + } + + void swap_swapsOwnership() const + { + const auto resource0 = GlobalResource::open(); + const auto resource1 = GlobalResource::open(); + + Handle h0{ resource0 }; + Handle h1{ resource1 }; + + std::swap(h0, h1); + + QCOMPARE_EQ(h0.get(), resource1); + QCOMPARE_EQ(h1.get(), resource0); + } + + void comparison_behavesAsInt_whenHandleTypeIsInt_data() const + { + QTest::addColumn("lhs"); + QTest::addColumn("rhs"); + + QTest::addRow("lhs == rhs") << 1 << 1; + QTest::addRow("lhs < rhs") << 0 << 1; + QTest::addRow("lhs > rhs") << 1 << 0; + } + + void comparison_behavesAsInt_whenHandleTypeIsInt() const + { + struct IntTraits + { + using Type = int; + + static bool close(Type handle) + { + return true; + } + + static Type invalidValue() noexcept + { + return INT_MAX; + } + }; + + using Handle = QUniqueHandle; + + QFETCH(int, lhs); + QFETCH(int, rhs); + + QCOMPARE_EQ(Handle{ lhs } == Handle{ rhs }, lhs == rhs); + QCOMPARE_EQ(Handle{ lhs } != Handle{ rhs }, lhs != rhs); + QCOMPARE_EQ(Handle{ lhs } < Handle{ rhs }, lhs < rhs); + QCOMPARE_EQ(Handle{ lhs } <= Handle{ rhs }, lhs <= rhs); + QCOMPARE_EQ(Handle{ lhs } > Handle{ rhs }, lhs > rhs); + QCOMPARE_EQ(Handle{ lhs } >= Handle{ rhs }, lhs >= rhs); + + QCOMPARE_EQ(Handle{ }, Handle{ }); + } + + void sort_sortsHandles() const + { + const auto resource0 = GlobalResource::open(); + const auto resource1 = GlobalResource::open(); + + QVERIFY(resource1 > resource0); // Precondition of underlying allocator + + Handle h0{ resource0 }; + Handle h1{ resource1 }; + + std::vector handles; + handles.push_back(std::move(h1)); + handles.push_back(std::move(h0)); + + std::sort(handles.begin(), handles.end()); + + QCOMPARE_LT(handles.front(), handles.back()); + QCOMPARE_LT(handles.front().get(), handles.back().get()); + } + + void addressOf_returnsAddressOfHandle() const + { + Handle h; + QVERIFY(GlobalResource::open(&h)); + QVERIFY(h.isValid()); + } + +}; + +// clang-format on +QTEST_MAIN(tst_QUniqueHandle) +#include "tst_quniquehandle.moc"