userver: userver/utils/lazy_shared_ptr.hpp Source File
Loading...
Searching...
No Matches
lazy_shared_ptr.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/utils/lazy_shared_ptr.hpp
4/// @brief @copybrief utils::LazySharedPtr
5
6#include <atomic>
7#include <functional>
8#include <memory>
9#include <utility>
10
11#include <userver/compiler/impl/lifetime.hpp>
12#include <userver/engine/sleep.hpp>
13#include <userver/utils/assert.hpp>
14#include <userver/utils/shared_readable_ptr.hpp>
15
16USERVER_NAMESPACE_BEGIN
17
18namespace utils {
19
20/// @ingroup userver_containers
21///
22/// @brief A lazy wrapper around utils::SharedReadablePtr that fetches the data
23/// on first access.
24///
25/// Provides standard thread-safety guarantee: `const` methods can be called
26/// concurrently. Once fetched, the same data is guaranteed to be returned
27/// unless the pointer is assigned to.
28template <typename T>
29class LazySharedPtr final {
30public:
31 /// @brief The default constructor, initializes with `nullptr`.
32 LazySharedPtr() noexcept : value_(nullptr), shared_filled_(true), get_data_() {}
33
34 /// @brief The non-lazy constructor
35 LazySharedPtr(utils::SharedReadablePtr<T> ptr) noexcept
36 : value_(ptr.Get()), shared_filled_(true), shared_(std::move(ptr)) {}
37
38 /// @brief The lazy constructor
39 LazySharedPtr(std::function<utils::SharedReadablePtr<T>()> function) noexcept : get_data_(std::move(function)) {}
40
41 /// @brief The copy constructor.
42 /// @note If `other` has not been fetched yet, `this` will not fetch
43 /// immediately, so `this` and `other` may end up pointing to different
44 /// objects.
45 LazySharedPtr(const LazySharedPtr& other)
46 : get_data_(other.get_data_)
47 {
48 if (other.shared_filled_.load()) {
49 value_.store(other.shared_.Get());
50 shared_ = other.shared_;
51 shared_filled_.store(true);
52 }
53 }
54
55 /// @brief The move constructor.
56 LazySharedPtr(LazySharedPtr&& other) noexcept
57 : value_(other.value_.load()),
58 shared_filled_(other.shared_filled_.load()),
59 shared_(std::move(other.shared_)),
60 get_data_(std::move(other.get_data_)) {}
61
62 /// @brief The copy assignment operator.
63 /// @note Like copy-constructor, it does not generate pointers if `other` do
64 /// not generate them.
65 LazySharedPtr& operator=(const LazySharedPtr& other) {
66 *this = LazySharedPtr(other);
67 return *this;
68 }
69
70 /// @brief The move assignment operator.
71 LazySharedPtr& operator=(LazySharedPtr&& other) noexcept {
72 value_.store(other.value_.load(), std::memory_order_relaxed);
73 shared_filled_.store(other.shared_filled_.load(), std::memory_order_relaxed);
74 shared_ = std::move(other.shared_);
75 get_data_ = std::move(other.get_data_);
76 }
77
78 /// @brief Get a pointer to the data (may be null). Fetches the data on the
79 /// first access.
80 const T* Get() const& USERVER_IMPL_LIFETIME_BOUND {
81 if (const auto* current_value = value_.load(); current_value != kUnfilled) {
82 return current_value;
83 }
84 auto readable = get_data_();
85 if (const auto* expected = kUnfilled; value_.compare_exchange_strong(expected, readable.Get())) {
86 shared_ = std::move(readable);
87 shared_filled_.store(true);
88 }
89 return value_;
90 }
91
92 /// @brief Get a smart pointer to the data (may be null). Fetches the data on
93 /// the first access.
94 const utils::SharedReadablePtr<T>& GetShared() const& USERVER_IMPL_LIFETIME_BOUND {
95 if (value_ == kUnfilled) {
96 auto readable = get_data_();
97 if (const auto* expected = kUnfilled; value_.compare_exchange_strong(expected, readable.Get())) {
98 shared_ = std::move(readable);
99 shared_filled_.store(true);
100 }
101 }
102 while (!shared_filled_.load()) {
103 // Another thread has filled 'value_', but not yet 'shared_'. It will be
104 // filled in a few CPU cycles. Discovering LazySharedPtr in such a state
105 // is expected to be rare.
106 engine::Yield();
107 }
108 return shared_;
109 }
110
111 ///@returns `*Get()`
112 ///@note `Get()` must not be `nullptr`.
113 const T& operator*() const& USERVER_IMPL_LIFETIME_BOUND {
114 const auto* res = Get();
115 UASSERT(res);
116 return *res;
117 }
118
119 /// @returns `Get()`
120 /// @note `Get()` must not be `nullptr`.
121 const T* operator->() const& USERVER_IMPL_LIFETIME_BOUND { return &**this; }
122
123 /// @returns `Get() != nullptr`
124 explicit operator bool() const& USERVER_IMPL_LIFETIME_BOUND { return !!Get(); }
125
126 /// @returns `GetShared()`
127 operator const utils::SharedReadablePtr<T>&() const& USERVER_IMPL_LIFETIME_BOUND { return GetShared(); }
128
129 /// @returns `GetShared()`
130 operator std::shared_ptr<const T>() const& USERVER_IMPL_LIFETIME_BOUND { return GetShared(); }
131
132private:
133 static inline const auto kUnfilled = reinterpret_cast<const T*>(std::uintptr_t{1});
134
135 mutable std::atomic<const T*> value_{kUnfilled};
136 mutable std::atomic<bool> shared_filled_{false};
137 mutable utils::SharedReadablePtr<T> shared_{nullptr};
138 std::function<utils::SharedReadablePtr<T>()> get_data_{};
139};
140
141/// @brief Make a lazy pointer to the data of a cache.
142///
143/// The cache type must have:
144/// - `DataType` member type;
145/// - `Get() -> utils::SharedReadablePtr<DataType>` method.
146///
147/// For example, components::CachingComponentBase satisfies these requirements.
148template <typename Cache>
149LazySharedPtr<typename Cache::DataType> MakeLazyCachePtr(Cache& cache) {
150 return utils::LazySharedPtr<typename Cache::DataType>([&cache] { return cache.Get(); });
151}
152
153} // namespace utils
154
155USERVER_NAMESPACE_END