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:
Jøger Hansegård 2023-10-31 19:35:07 +01:00
parent bb6ed27b50
commit 8d367dec15
5 changed files with 550 additions and 0 deletions

View File

@ -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

View 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

View File

@ -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)

View 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
)

View 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"