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 {
29public:
30 /// @brief The default constructor, initializes with `nullptr`.
31 LazySharedPtr() noexcept : value_(nullptr), shared_filled_(true), get_data_() {}
32
33 /// @brief The non-lazy constructor
34 LazySharedPtr(utils::SharedReadablePtr<T> ptr) noexcept
35 : value_(ptr.Get()), shared_filled_(true), shared_(std::move(ptr)) {}
36
37 /// @brief The lazy constructor
38 LazySharedPtr(std::function<utils::SharedReadablePtr<T>()> function) noexcept : get_data_(std::move(function)) {}
39
40 /// @brief The copy constructor.
41 /// @note If `other` has not been fetched yet, `this` will not fetch
42 /// immediately, so `this` and `other` may end up pointing to different
43 /// objects.
44 LazySharedPtr(const LazySharedPtr& other) : get_data_(other.get_data_) {
45 if (other.shared_filled_.load()) {
46 value_.store(other.shared_.Get());
47 shared_ = other.shared_;
48 shared_filled_.store(true);
49 }
50 }
51
52 /// @brief The move constructor.
53 LazySharedPtr(LazySharedPtr&& other) noexcept
54 : value_(other.value_.load()),
55 shared_filled_(other.shared_filled_.load()),
56 shared_(std::move(other.shared_)),
57 get_data_(std::move(other.get_data_)) {}
58
59 /// @brief The copy assignment operator.
60 /// @note Like copy-constructor, it does not generate pointers if `other` do
61 /// not generate them.
62 LazySharedPtr& operator=(const LazySharedPtr& other) {
63 *this = LazySharedPtr(other);
64 return *this;
65 }
66
67 /// @brief The move assignment operator.
68 LazySharedPtr& operator=(LazySharedPtr&& other) noexcept {
69 value_.store(other.value_.load(), std::memory_order_relaxed);
70 shared_filled_.store(other.shared_filled_.load(), std::memory_order_relaxed);
71 shared_ = std::move(other.shared_);
72 get_data_ = std::move(other.get_data_);
73 }
74
75 /// @brief Get a pointer to the data (may be null). Fetches the data on the
76 /// first access.
77 const T* Get() const& {
78 if (const auto* current_value = value_.load(); current_value != kUnfilled) {
79 return current_value;
80 }
81 auto readable = get_data_();
82 if (const auto* expected = kUnfilled; value_.compare_exchange_strong(expected, readable.Get())) {
83 shared_ = std::move(readable);
84 shared_filled_.store(true);
85 }
86 return value_;
87 }
88
89 /// @brief Get a smart pointer to the data (may be null). Fetches the data on
90 /// the first access.
91 const utils::SharedReadablePtr<T>& GetShared() const& {
92 if (value_ == kUnfilled) {
93 auto readable = get_data_();
94 if (const auto* expected = kUnfilled; value_.compare_exchange_strong(expected, readable.Get())) {
95 shared_ = std::move(readable);
96 shared_filled_.store(true);
97 }
98 }
99 while (!shared_filled_.load()) {
100 // Another thread has filled 'value_', but not yet 'shared_'. It will be
101 // filled in a few CPU cycles. Discovering LazySharedPtr in such a state
102 // is expected to be rare.
103 engine::Yield();
104 }
105 return shared_;
106 }
107
108 ///@returns `*Get()`
109 ///@note `Get()` must not be `nullptr`.
110 const T& operator*() const& {
111 const auto* res = Get();
112 UASSERT(res);
113 return *res;
114 }
115
116 /// @returns `Get()`
117 /// @note `Get()` must not be `nullptr`.
118 const T* operator->() const& { return &**this; }
119
120 /// @returns `Get() != nullptr`
121 explicit operator bool() const& { return !!Get(); }
122
123 /// @returns `GetShared()`
124 operator const utils::SharedReadablePtr<T>&() const& { return GetShared(); }
125
126 /// @returns `GetShared()`
127 operator std::shared_ptr<const T>() const& { return GetShared(); }
128
129private:
130 static inline const auto kUnfilled = reinterpret_cast<const T*>(std::uintptr_t{1});
131
132 mutable std::atomic<const T*> value_{kUnfilled};
133 mutable std::atomic<bool> shared_filled_{false};
134 mutable utils::SharedReadablePtr<T> shared_{nullptr};
135 std::function<utils::SharedReadablePtr<T>()> get_data_{};
136};
137
138/// @brief Make a lazy pointer to the data of a cache.
139///
140/// The cache type must have:
141/// - `DataType` member type;
142/// - `Get() -> utils::SharedReadablePtr<DataType>` method.
143///
144/// For example, components::CachingComponentBase satisfies these requirements.
145template <typename Cache>
146LazySharedPtr<typename Cache::DataType> MakeLazyCachePtr(Cache& cache) {
147 return utils::LazySharedPtr<typename Cache::DataType>([&cache] { return cache.Get(); });
148}
149
150} // namespace utils
151
152USERVER_NAMESPACE_END