Add QUniqueHandle - a general purpose RAII wrapper for non-memory types
When interfacing with C-style APIs, such as the Windows API, resources are often represented using handle objects. Lifetime management of such resources can be cumbersome and error prone, because typical handle objects (ints) do not give any help to release resources, and to manage ownership. Although std::unique_ptr can be retro-fitted with a custom deleter, and helps transfer of ownership, it is inherently a pointer type. It can therefore be clumsy to use with C-style APIs, particularly if the invalid (uninitialized) handle value is not a nullptr. Also, the std::unique_ptr does not work well when an allocating function returns the handle as a pointer argument. The QUniqueHandle addresses these issues by providing a movable only value type that is designed as a RAII handle wrapper. A similar handle wrapper exists in the Windows SDK, as part of the WRL library. Unfortunately, this is Microsoft specific, and is not supported by MINGW. Since the QUniqueHandle is platform independent, it can be used also with non- Microsoft platforms, and can be useful with other C-style APIs such as FFmpeg or SQLite. Pick-to: 6.6 6.5 Change-Id: Ibfc0cec3f361ec004febea5f284ebf75e27c0054 Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io> Reviewed-by: Thiago Macieira <thiago.macieira@intel.com> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
parent
bb6ed27b50
commit
8d367dec15
@ -281,6 +281,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
|
||||
|
225
src/corelib/tools/quniquehandle_p.h
Normal file
225
src/corelib/tools/quniquehandle_p.h
Normal file
@ -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 <QtCore/qtconfigmacros.h>
|
||||
#include <QtCore/qassert.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
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<InvalidHandleTraits>;
|
||||
|
||||
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<SqLiteTraits>;
|
||||
|
||||
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 <typename HandleTraits>
|
||||
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
|
@ -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)
|
||||
|
15
tests/auto/corelib/tools/quniquehandle/CMakeLists.txt
Normal file
15
tests/auto/corelib/tools/quniquehandle/CMakeLists.txt
Normal file
@ -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
|
||||
)
|
308
tests/auto/corelib/tools/quniquehandle/tst_quniquehandle.cpp
Normal file
308
tests/auto/corelib/tools/quniquehandle/tst_quniquehandle.cpp
Normal file
@ -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 <private/quniquehandle_p.h>
|
||||
|
||||
#include <QTest>
|
||||
|
||||
QT_USE_NAMESPACE;
|
||||
|
||||
// clang-format off
|
||||
namespace GlobalResource {
|
||||
|
||||
std::array<bool, 3> s_resources = { false, false, false };
|
||||
|
||||
using handle = size_t;
|
||||
constexpr handle s_invalidHandle = static_cast<handle>(-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<TestTraits>;
|
||||
|
||||
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<Handle> && !std::is_copy_assignable_v<Handle>);
|
||||
}
|
||||
|
||||
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<int>("lhs");
|
||||
QTest::addColumn<int>("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<IntTraits>;
|
||||
|
||||
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<Handle> 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"
|
Loading…
Reference in New Issue
Block a user