• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DNKpp / Simple-Utility / 6500581756

12 Oct 2023 08:26PM UTC coverage: 86.926% (-12.8%) from 99.712%
6500581756

Pull #71

github

web-flow
Merge 1fb43c509 into e4166ac5d
Pull Request #71: graph namespace

284 of 284 new or added lines in 17 files covered. (100.0%)

625 of 719 relevant lines covered (86.93%)

154.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

17.57
/include/Simple-Utility/unique_handle.hpp
1
//          Copyright Dominic Koepke 2019 - 2023.
2
// Distributed under the Boost Software License, Version 1.0.
3
//    (See accompanying file LICENSE_1_0.txt or copy at
4
//          https://www.boost.org/LICENSE_1_0.txt)
5

6
#ifndef SL_UNIQUE_HANDLE_HPP
7
#define SL_UNIQUE_HANDLE_HPP
8

9
#pragma once
10

11
#include <compare>
12
#include <concepts>
13
#include <functional>
14
#include <optional>
15
#include <utility>
16

17
#include "Simple-Utility/Config.hpp"
18
#include "Simple-Utility/concepts/stl_extensions.hpp"
19
#include "Simple-Utility/nullables/base.hpp"
20

21
// some of the std::optional interface hasn't been declared constexpr before
22
#if __cpp_lib_optional >= 202106L
23
#define SL_UNIQUE_HANDLE_FULL_CONSTEXPR constexpr
24
#else
25
#define SL_UNIQUE_HANDLE_FULL_CONSTEXPR
26
#endif
27

28
namespace sl
29
{
30
        /*! \defgroup GROUP_UNIQUE_HANDLE unique_handle
31
        *
32
        * \brief A helper type, which acts as a nullable resource handle with self-cleanup support.
33
        *
34
        * \details The class \ref unique_handle is in fact a wrapper around a ``std::optional``, thus has at least the overhead of that. Additionally it adds two
35
        * important aspects:
36
        *                -# it resets its internal value after it got moved.
37
        *                -# it invokes its delete action every time when the internal value switches its state from initialized to uninitialized.
38
        *
39
        * The latter happens when the value is in an initialized state and the \ref unique_handle gets
40
        *                - moved,
41
        *                - destructed or
42
        *                - assigned
43
        *
44
        * This is very similar to the behaviour of a ``std::unique_ptr``, hence the name.
45
        * This behaviour is useful in cases when one has an identifier to a resource, which isn't stored on the heap (thus a ``std::unique_ptr`` is not
46
        * a good option), and this identifier should have the responsibility as an owner over that resource, but the resource itself is not bound to the
47
        * lifetime of that identifier. This might sound quite abstract, thus let us visit a simple example.
48
        *
49
        * Here some entities are stored in a simple ``std::list``. Imagine this entities are accessible from many places in your program, thus something
50
        * like this can easily happen.
51
        *
52
        * \code{.cpp}
53
        *        std::list<Entity> entities{};
54
        *
55
        *        {
56
        *                entities.emplace_front();
57
        *                auto entity_id = entities.begin();
58
        *
59
        *                // do some actions with entity and other stuff
60
        *
61
        *                // entity should now be erased. Not actually c++-ig, is it?
62
        *                entities.erase(entity_id);
63
        *        }
64
        * \endcode
65
        *
66
        * This is clearly no ``memory leak`` but if one forgets to erase the entity, it exists until the list is cleared.
67
        *        With \ref unique_handle one can do this.
68
        *
69
        * \code{.cpp}
70
        *        struct list_delete_action
71
        *        {
72
        *                std::list<Entity>* list{};  // pointer here, because a delete action must be move and copyable
73
        *
74
        *                void operator ()(const std::list<Entity>::iterator& itr) { list->erase(itr); }
75
        *        };
76
        *        std::list<Entity> entities{};
77
        *
78
        *        {
79
        *                entities.emplace_front();
80
        *                sl::unique_handle entity_id{ entities.begin(), list_delete_action{ &entities } };
81
        *
82
        *                // do some actions with entity and other stuff
83
        *
84
        *                // no cleanup necessary
85
        *        }
86
        * \endcode
87
        *
88
        * Of course, at a first glance this seems quite more verbose, but in the long term nobody has to care about that entity anymore. This is what ``RAII`` is about.
89
        * Note that ``unique_handles`` also can be stored as a member, then they really begin to shine, because if one would like to bind that entity to the lifetime of
90
        * an other object that would of course lead to custom move constructor, assignment operator and destructor and explicitly deleted copy. With a
91
        * \ref unique_handle none of this is necessary (and this is in fact the main reason why I decided to implement this).
92
        *
93
        * \note As using lambdas as delete action is usually fine, using capturing lambdas will fail to compile because they are non-copy-assignable. Use a old-school
94
        * invokable struct as in the example instead.
95
        *
96
        * \see https://en.cppreference.com/w/cpp/utility/optional
97
        * \see https://en.cppreference.com/w/cpp/memory/unique_ptr
98
        * \see https://en.cppreference.com/w/cpp/language/lambda
99
        *
100
        * @{
101
        */
102

103
        /**
104
         * \brief helper type for indicating unique_handles with uninitialized state
105
         */
106
        // ReSharper disable once IdentifierTypo
107
        struct nullhandle_t
108
        { };
109

110
        /**
111
         * \brief an object of type nullhandle_t
112
         */
113
        // ReSharper disable once IdentifierTypo
114
        constexpr nullhandle_t nullhandle{};
115

116
        /**
117
         * \brief exception type which indicates checked access to an uninitialized value
118
         */
119
        using bad_handle_access = std::bad_optional_access;
120

121
        /**
122
         * \brief default delete action for unique_handle with an empty operator ()
123
         */
124
        struct default_delete_action
125
        {
126
                /**
127
                 * \brief Empty invoke operator
128
                 */
129
                constexpr void operator ()(auto&) const noexcept
×
130
                { }
×
131
        };
132

133
        namespace detail
134
        {
135
                template <class T>
136
                struct value_validator
137
                {
138
                        static_assert(std::movable<T>, "The value type must be movable.");
139
                        static_assert(concepts::not_same_as<std::remove_cvref_t<T>, nullhandle_t>, "The value type must not be sl::nullhandle_t.");
140

141
                        using type = T;
142
                };
143

144
                template <class T, class TDeleteAction>
145
                struct delete_action_validator
146
                {
147
                        static_assert(std::copyable<TDeleteAction>, "The delete action object must be copyable (capturing lambdas are not).");
148
                        static_assert(std::invocable<TDeleteAction, T&>, "The delete action object must be invokable by T&.");
149

150
                        using type = TDeleteAction;
151
                };
152

153
                template <class T>
154
                using type_t = typename T::type;
155
        }
156

157
        /**
158
         * \brief Checks whether the given template type is usable as value type for ``unique_handle`` types.
159
         */
160
        template <class T>
161
        concept value = std::movable<T> && concepts::not_same_as<T, nullhandle_t>;
162

163
        /**
164
         * \brief Checks whether the given template type is usable as delete action type for ``unique_handle`` types.
165
         */
166
        template <class T, class TValue>
167
        concept delete_action_for = value<TValue>
168
                                                        && std::invocable<T, TValue&>
169
                                                        && std::copyable<T>;
170

171
        /**
172
         * \brief This type models some kind of ``std::optional`` behaviour but resets itself on move operations.
173
         * \details For more details and information about related components go to \ref GROUP_UNIQUE_HANDLE "unique_handle group page".
174
         * \tparam T The type of the stored value
175
         * \tparam TDeleteAction Type of the used delete action
176
         */
177
        template <value T, delete_action_for<T> TDeleteAction = default_delete_action>
178
        class unique_handle
179
        {
180
        private:
181
                template <value T2, delete_action_for<T2> TOtherDeleteAction>
182
                friend class unique_handle;
183

184
        public:
185
                /**
186
                 * \brief Type of the stored value
187
                 */
188
                using value_type = T;
189
                /**
190
                 * \brief Type of the used delete action
191
                 */
192
                using delete_action_type = TDeleteAction;
193

194
                /**
195
                 * \brief Default constructor. The value will be in an uninitialized stated and the delete action gets default constructed.
196
                 */
197
                constexpr unique_handle() noexcept = default;
×
198

199
                /**
200
                 * \brief Destruct. Does invoke the delete action if the value is in an initialized state.
201
                 */
202
                constexpr ~unique_handle() noexcept
2✔
203
                {
204
                        invoke_delete_action_if_necessary();
2✔
205
                }
2✔
206

207
                /**
208
                 * \brief Move constructor, which relocates the ownership of the value to the target and resets the source. Delete actions will be copied.
209
                 * \param other The source object which will lose ownership, if it has any.
210
                 */
211
                SL_UNIQUE_HANDLE_FULL_CONSTEXPR unique_handle
×
212
                (
213
                        unique_handle&& other
214
                ) noexcept(std::is_nothrow_move_constructible_v<T>
215
                                        && std::is_nothrow_copy_constructible_v<TDeleteAction>)
216
                        : m_Value{ std::exchange(other.m_Value, std::nullopt) },
×
217
                        m_DeleteAction{ other.m_DeleteAction }
×
218
                { }
×
219

220
                /**
221
                 * \brief Move assignment, which relocates the ownership of the value to the target and resets the source. Delete actions will be copied.
222
                 * \param other The source object which will lose ownership, if it has any.
223
                 * \return A reference to the target object
224
                 */
225
                SL_UNIQUE_HANDLE_FULL_CONSTEXPR unique_handle& operator =
×
226
                (
227
                        unique_handle&& other
228
                ) noexcept(std::is_nothrow_move_constructible_v<T>
229
                                        && std::is_nothrow_move_assignable_v<T>
230
                                        && std::is_nothrow_copy_constructible_v<TDeleteAction>
231
                                        && std::is_nothrow_copy_assignable_v<TDeleteAction>)
232
                {
233
                        if (this != &other)
×
234
                        {
235
                                invoke_delete_action_if_necessary();
×
236

237
                                m_Value = std::exchange(other.m_Value, std::nullopt);
×
238
                                m_DeleteAction = other.m_DeleteAction;
×
239
                        }
240
                        return *this;
×
241
                }
242

243
                /**
244
                 * \brief Swaps the target and the source in a more efficient way.
245
                 * \param other The source object
246
                 */
247
                constexpr void swap
×
248
                (
249
                        unique_handle& other
250
                ) noexcept(std::is_nothrow_move_constructible_v<T>
251
                                        && std::is_nothrow_swappable_v<T>
252
                                        && std::is_nothrow_move_constructible_v<TDeleteAction>
253
                                        && std::is_nothrow_swappable_v<TDeleteAction>)
254
                {
255
                        using std::swap;
256

257
                        swap(m_Value, other.m_Value);
×
258
                        swap(m_DeleteAction, other.m_DeleteAction);
×
259
                }
260

261
                /**
262
                 * \brief Explicitly deleted copy constructor.
263
                 */
264
                unique_handle(const unique_handle&) = delete;
265
                /**
266
                 * \brief Explicitly deleted copy assignment.
267
                 * \return nothing
268
                 */
269
                unique_handle& operator =(const unique_handle&) = delete;
270

271
                /**
272
                 * \brief Explicitly does not initialize the value. This overload is mainly used for convenience when returning
273
                 * a \ref nullhandle.
274
                 * \param deleteAction The provided delete action object
275
                 */
276
                constexpr unique_handle(nullhandle_t, const delete_action_type& deleteAction = delete_action_type()) noexcept
×
277
                        : m_Value{ std::nullopt },
×
278
                        m_DeleteAction{ deleteAction }
×
279
                { }
×
280

281
                /**
282
                 * \brief Explicitly resets the value and invokes the delete action if value was initialized.
283
                 */
284
                SL_UNIQUE_HANDLE_FULL_CONSTEXPR unique_handle& operator =(nullhandle_t) noexcept
×
285
                {
286
                        invoke_delete_action_if_necessary();
×
287

288
                        m_Value = std::nullopt;
×
289
                        return *this;
×
290
                }
291

292
                /**
293
                 * \brief Constructor overload for explicitly providing a delete action.
294
                 * \param deleteAction The provided delete action object
295
                 */
296
                constexpr unique_handle(const delete_action_type& deleteAction) noexcept
×
297
                        : m_Value{ std::nullopt },
×
298
                        m_DeleteAction{ deleteAction }
×
299
                { }
×
300

301
                /**
302
                 * \brief Constructor overload for initializing the value.
303
                 * \tparam T2 Type of the provided value. Must be convertible to ``T``.
304
                 * \param value Used object to initialize the value
305
                 * \param deleteAction The provided delete action object
306
                 */
307
                template <concepts::initializes<T> T2>
308
                        requires concepts::not_same_as<std::remove_cvref_t<T2>, unique_handle>
309
                                        && concepts::not_same_as<std::remove_cvref_t<T2>, nullhandle_t>
310
                explicit (!std::convertible_to<T2&&, T>)
311
                constexpr unique_handle(T2&& value, const delete_action_type& deleteAction = delete_action_type{})
×
312
                        : m_Value{ std::forward<T2>(value) },
×
313
                        m_DeleteAction{ deleteAction }
×
314
                { }
×
315

316
                /**
317
                 * \brief Assignment operator overload for assigning the value.
318
                 * \tparam T2 Type of the provided value. Must be convertible to ``T``.
319
                 * \param value Used object to assign the value
320
                 * \return A reference to this
321
                 */
322
                template <concepts::assignable_to<T&> T2>
323
                        requires concepts::not_same_as<std::remove_cvref_t<T2>, unique_handle>
324
                                        && concepts::not_same_as<std::remove_cvref_t<T2>, nullhandle_t>
325
                                        && concepts::initializes<std::remove_cvref_t<T2>, T>
326
                constexpr unique_handle& operator =(T2&& value)
×
327
                {
328
                        invoke_delete_action_if_necessary();
×
329

330
                        m_Value = std::forward<T2>(value);
×
331
                        return *this;
×
332
                }
333

334
                /**
335
                 * \brief Constructor overload for directly initializing the value with a set of arguments.
336
                 * \tparam TArgs Type of the provided arguments.
337
                 * \param args Used arguments to initialize the value
338
                 */
339
                template <class... TArgs>
340
                        requires std::constructible_from<T, TArgs...>
341
                constexpr explicit unique_handle(std::in_place_t, TArgs&&... args)
×
342
                        : m_Value{ std::in_place, std::forward<TArgs>(args)... }
×
343
                { }
×
344

345
                /**
346
                 * \brief Constructor overload for directly initializing the value with a set of arguments and also initializing
347
                 * the delete action.
348
                 * \tparam TArgs Type of the provided arguments.
349
                 * \param deleteAction The provided delete action object
350
                 * \param args Used arguments to initialize the value
351
                 */
352
                template <class... TArgs>
353
                        requires std::constructible_from<T, TArgs...>
354
                constexpr explicit unique_handle(std::in_place_t, const delete_action_type& deleteAction, TArgs&&... args)
×
355
                        : m_Value{ std::in_place, std::forward<TArgs>(args)... },
×
356
                        m_DeleteAction{ deleteAction }
×
357
                { }
×
358

359
                /**
360
                 * \brief Constructor overload for directly initializing the value with a set of arguments.
361
                 * \tparam TArgs Type of the provided arguments.
362
                 * \param args Used arguments to initialize the value
363
                 */
364
                template <class... TArgs>
365
                        requires std::constructible_from<T, TArgs...>
366
                SL_UNIQUE_HANDLE_FULL_CONSTEXPR void emplace(TArgs&&... args)
×
367
                {
368
                        m_Value.emplace(std::forward<TArgs>(args)...);
×
369
                }
370

371
                /**
372
                 * \brief Resets the value and invokes the delete action if value was initialized.
373
                 */
374
                SL_UNIQUE_HANDLE_FULL_CONSTEXPR void reset() noexcept
1✔
375
                {
376
                        invoke_delete_action_if_necessary();
1✔
377

378
                        m_Value.reset();
1✔
379
                }
1✔
380

381
                /**
382
                 * \brief Immutable access to the value. No checks will be performed.
383
                 * \exception Throws bad_handle_access if value is uninitialized.
384
                 * \return A const reference to the value
385
                 */
386
                [[nodiscard]]
387
                constexpr const T& raw() const { return m_Value.value(); }
1✔
388

389
                /**
390
                 * \brief Immutable access to the value. No checks will be performed.
391
                 * \return A const reference to the value
392
                 */
393
                [[nodiscard]]
394
                constexpr const T& operator *() const noexcept { return *m_Value; }
×
395

396
                /**
397
                 * \brief Immutable access to the value. No checks will be performed.
398
                 * \return A const pointer to the value
399
                 */
400
                [[nodiscard]]
401
                constexpr const T* operator ->() const noexcept { return &*m_Value; }
×
402

403
                /**
404
                 * \brief Checks whether the value is initialized.
405
                 * \return True if value is initialized
406
                 */
407
                [[nodiscard]]
408
                constexpr explicit operator bool() const noexcept { return m_Value.has_value(); }
2✔
409

410
                /**
411
                 * \brief Checks whether the value is initialized.
412
                 * \return True if value is initialized
413
                 */
414
                [[nodiscard]]
415
                constexpr bool is_valid() const noexcept { return m_Value.has_value(); }
4✔
416

417
                /**
418
                 * \brief Immutable access to the delete action.
419
                 * \return A const reference to the delete action
420
                 */
421
                [[nodiscard]]
422
                constexpr const delete_action_type& delete_action() const noexcept { return m_DeleteAction; }
×
423

424
                /**
425
                 * \brief Equality-comparison operator overload between two \ref unique_handle "unique_handles". operator != is implicitly defined.
426
                 * \tparam T2 Other value type.
427
                 * \tparam TOtherDeleteAction Other delete action type.
428
                 * \param other The ``unique_handle`` to compare with
429
                 * \return Both sides compare equal if both handles contain uninitialized values or both value are initialized and compare equal.
430
                 * Otherwise ``false`` is returned.
431
                 */
432
                template <std::equality_comparable_with<T> T2, class TOtherDeleteAction>
433
                [[nodiscard]]
434
                constexpr bool operator ==(const unique_handle<T2, TOtherDeleteAction>& other) const
×
435
                {
436
                        return m_Value == other.m_Value;
×
437
                }
438

439
                /**
440
                 * \brief Three-way-comparison operator overload between two \ref unique_handle "unique_handles".
441
                 * \tparam T2 Other value type.
442
                 * \tparam TOtherDeleteAction Other delete action type.
443
                 * \param other The object to compare with
444
                 * \return If both handles have initialized values, both values will be compared. Otherwise they will be compared in accordance
445
                 * to the result of ``is_valid()``.
446
                 */
447
                template <std::three_way_comparable_with<T> T2, class TOtherDeleteAction>
448
                [[nodiscard]]
449
                constexpr std::compare_three_way_result_t<T, T2> operator <=>(const unique_handle<T2, TOtherDeleteAction>& other) const
×
450
                {
451
                        return m_Value <=> other.m_Value;
×
452
                }
453

454
                /**
455
                 * \brief Three-way-comparison operator overload for comparison between a \ref unique_handle and a value.
456
                 * \tparam T2 Type of right-hand-side. Must be three-way-comparable to ``T``
457
                 * \param other The object to compare with
458
                 * \return If the handle's value is initialized, both values will be compared. Otherwise handle is less.
459
                 */
460
                template <class T2>
461
                        requires concepts::not_same_as<std::remove_cvref_t<T2>, unique_handle>
462
                                        // do not move into template arg list, as this will result in a template cycle
463
                                        && std::three_way_comparable_with<T, T2>
464
                [[nodiscard]]
465
                constexpr std::compare_three_way_result_t<T, T2> operator <=>(const T2& other) const
×
466
                {
467
                        return m_Value <=> other;
×
468
                }
469

470
                /**
471
                 * \brief Equality-comparison operator overload for comparison between a \ref unique_handle and a value. operator != is implicitly defined.
472
                 * \tparam T2 Type of right-hand-side. Must be equality-comparable to ``T``
473
                 * \param other The object to compare with
474
                 * \return Returns ``true`` if handle is valid and value compares equal equal to rhs. Otherwise ``false``.
475
                 */
476
                template <class T2>
477
                        requires concepts::not_same_as<std::remove_cvref_t<T2>, unique_handle>
478
                                        // do not move into template arg list, as this will result in a template cycle
479
                                        && std::equality_comparable_with<T, T2>
480
                [[nodiscard]]
481
                constexpr bool operator ==(const T2& other) const
×
482
                {
483
                        return m_Value == other;
×
484
                }
485

486
                /**
487
                 * \brief Equality-comparison operator overload for comparison of \ref unique_handle and \ref nullhandle_t.
488
                 * \return Returns true if handle's value is uninitialized.
489
                 */
490
                [[nodiscard]]
491
                constexpr bool operator ==(nullhandle_t) const noexcept
×
492
                {
493
                        return !is_valid();
×
494
                }
495

496
                /**
497
                 * \brief Three-way-comparison operator overload for comparison of \ref unique_handle and \ref nullhandle_t.
498
                 * \return Returns ``std::strong_ordering::equal`` if handle's value is uninitialized, otherwise ``std::strong_ordering::greater``.
499
                 */
500
                [[nodiscard]]
501
                constexpr std::strong_ordering operator <=>(nullhandle_t) const noexcept
×
502
                {
503
                        return is_valid() <=> false;
×
504
                }
505

506
        private:
507
                std::optional<T> m_Value{};
508

509
                SL_UTILITY_NO_UNIQUE_ADDRESS
510
                TDeleteAction m_DeleteAction{};
511

512
                constexpr void invoke_delete_action_if_necessary() noexcept
4✔
513
                {
514
                        if (m_Value)
4✔
515
                                std::invoke(m_DeleteAction, *m_Value);
×
516
                }
4✔
517
        };
518

519
        /**
520
         * \brief Deduction guide for \ref unique_handle class
521
         * \relatesalso unique_handle
522
         * \tparam T Value type
523
         * \tparam TDeleteAction Delete action type
524
         */
525
        template <class T, class TDeleteAction>
526
        unique_handle
527
        (
528
                T,
529
                TDeleteAction
530
        ) -> unique_handle<detail::type_t<detail::value_validator<T>>, detail::type_t<detail::delete_action_validator<T, TDeleteAction>>>;
531

532
        /**
533
         * \brief Deduction guide for \ref unique_handle class
534
         * \relatesalso unique_handle
535
         * \tparam T Value type
536
         */
537
        template <class T>
538
        unique_handle(T) -> unique_handle<T>;
539

540
        /**
541
         * \brief Specialization for \ref sl::unique_handle "unique_handle" types.
542
         * \ingroup GROUP_NULLABLES_TRAITS
543
         * \tparam T Value type.
544
         * \tparam TDeleteAction Delete action type.
545
         */
546
        template <class T, class TDeleteAction>
547
        struct nullables::traits<unique_handle<T, TDeleteAction>>
548
        {
549
                using value_type = typename unique_handle<T, TDeleteAction>::value_type;
550
                static constexpr nullhandle_t null{ nullhandle };
551
        };
552

553
        /** @} */
554
}
555

556
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc