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