userver: userver/utils/trivial_map.hpp Source File
⚠️ This is the documentation for an old userver version. Click here to switch to the latest version.
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
trivial_map.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/utils/trivial_map.hpp
4/// @brief Bidirectional map|sets over string literals or other trivial types.
5
6#include <cstddef>
7#include <optional>
8#include <string>
9#include <string_view>
10#include <type_traits>
11#include <utility>
12#include <variant>
13
14#include <fmt/format.h>
15
16#include <userver/compiler/demangle.hpp>
17#include <userver/utils/assert.hpp>
18
19USERVER_NAMESPACE_BEGIN
20
21namespace utils {
22
23namespace impl {
24
25constexpr bool HasUppercaseAscii(std::string_view value) noexcept {
26 for (auto c : value) {
27 if ('A' <= c && c <= 'Z') return true;
28 }
29
30 return false;
31}
32
33constexpr bool ICaseEqualLowercase(std::string_view lowercase,
34 std::string_view y) noexcept {
35 const auto size = lowercase.size();
36 UASSERT(size == y.size());
37 constexpr char kLowerToUpperMask = static_cast<char>(~unsigned{32});
38 for (std::size_t i = 0; i < size; ++i) {
39 const auto lowercase_c = lowercase[i];
40 UASSERT(!('A' <= lowercase_c && lowercase_c <= 'Z'));
41 if (lowercase_c != y[i]) {
42 if (!('a' <= lowercase_c && lowercase_c <= 'z') ||
43 (lowercase_c & kLowerToUpperMask) != y[i]) {
44 return false;
45 }
46 }
47 }
48
49 return true;
50}
51
52struct Found final {
53 constexpr explicit Found(std::size_t value) noexcept { UASSERT(value == 0); }
54
55 constexpr explicit operator std::size_t() const noexcept { return 0; }
56};
57
58template <typename Key, typename Value, typename Enabled = void>
59class SearchState final {
60 public:
61 constexpr explicit SearchState(Key key) noexcept
62 : key_or_result_(std::in_place_index<0>, key) {}
63
64 constexpr bool IsFound() const noexcept {
65 return key_or_result_.index() != 0;
66 }
67
68 constexpr Key GetKey() const noexcept {
69 UASSERT(!IsFound());
70 return std::get<0>(key_or_result_);
71 }
72
73 constexpr void SetValue(Value value) noexcept {
74 key_or_result_ = std::variant<Key, Value>(std::in_place_index<1>, value);
75 }
76
77 [[nodiscard]] constexpr std::optional<Value> Extract() noexcept {
78 if (key_or_result_.index() == 1) {
79 return std::get<1>(key_or_result_);
80 } else {
81 return std::nullopt;
82 }
83 }
84
85 private:
86 std::variant<Key, Value> key_or_result_;
87};
88
89inline constexpr std::size_t kInvalidSize =
90 std::numeric_limits<std::size_t>::max();
91
92template <typename Payload>
93inline constexpr bool kFitsInStringOrPayload =
94 sizeof(Payload) <= sizeof(const char*) &&
95 (std::is_integral_v<Payload> || std::is_enum_v<Payload> ||
96 std::is_same_v<Payload, Found>);
97
98// A compacted std::variant<std::string_view, Payload>
99template <typename Payload>
100class StringOrPayload final {
101 public:
102 constexpr explicit StringOrPayload(std::string_view string) noexcept
103 : data_or_payload_(string.data()), size_(string.size()) {
104#if defined(__clang__)
105 __builtin_assume(size_ != kInvalidSize);
106#elif defined(__GNUC__)
107 if (size_ == kInvalidSize) __builtin_unreachable();
108#endif
109 }
110
111 constexpr explicit StringOrPayload(Payload payload) noexcept
112 : data_or_payload_(payload), size_(kInvalidSize) {}
113
114 constexpr bool HasPayload() const noexcept { return size_ == kInvalidSize; }
115
116 constexpr std::string_view GetString() const noexcept {
117 UASSERT(!HasPayload());
118 return std::string_view{data_or_payload_.data, size_};
119 }
120
121 constexpr Payload GetPayload() const noexcept {
122 UASSERT(HasPayload());
123 return data_or_payload_.payload;
124 }
125
126 private:
127 static_assert(kFitsInStringOrPayload<Payload>);
128
129 union DataOrPayload {
130 constexpr explicit DataOrPayload(const char* data) noexcept : data(data) {}
131
132 constexpr explicit DataOrPayload(Payload payload) noexcept
133 : payload(payload) {}
134
135 const char* data{};
136 Payload payload;
137 };
138
139 DataOrPayload data_or_payload_;
140 std::size_t size_;
141};
142
143template <typename Value>
144class SearchState<std::string_view, Value,
145 std::enable_if_t<kFitsInStringOrPayload<Value>>>
146 final {
147 public:
148 constexpr explicit SearchState(std::string_view key) noexcept : state_(key) {}
149
150 constexpr bool IsFound() const noexcept { return state_.HasPayload(); }
151
152 constexpr std::string_view GetKey() const noexcept {
153 return state_.GetString();
154 }
155
156 constexpr void SetValue(Value value) noexcept {
157 state_ = StringOrPayload<Value>{value};
158 }
159
160 [[nodiscard]] constexpr std::optional<Value> Extract() noexcept {
161 return IsFound() ? std::optional{state_.GetPayload()} : std::nullopt;
162 }
163
164 private:
165 StringOrPayload<Value> state_;
166};
167
168template <typename Key>
169class SearchState<Key, std::string_view,
170 std::enable_if_t<kFitsInStringOrPayload<Key>>>
171 final {
172 public:
173 constexpr explicit SearchState(Key key) noexcept : state_(key) {}
174
175 constexpr bool IsFound() const noexcept { return !state_.HasPayload(); }
176
177 constexpr Key GetKey() const noexcept { return state_.GetPayload(); }
178
179 constexpr void SetValue(std::string_view value) noexcept {
180 state_ = StringOrPayload<Key>{value};
181 }
182
183 [[nodiscard]] constexpr std::optional<std::string_view> Extract() noexcept {
184 return IsFound() ? std::optional{state_.GetString()} : std::nullopt;
185 }
186
187 private:
188 StringOrPayload<Key> state_;
189};
190
191template <typename First, typename Second>
192class SwitchByFirst final {
193 public:
194 constexpr explicit SwitchByFirst(First search) noexcept : state_(search) {}
195
196 constexpr SwitchByFirst& Case(First first, Second second) noexcept {
197 if (!state_.IsFound() && state_.GetKey() == first) {
198 state_.SetValue(second);
199 }
200 return *this;
201 }
202
203 [[nodiscard]] constexpr std::optional<Second> Extract() noexcept {
204 return state_.Extract();
205 }
206
207 private:
208 SearchState<First, Second> state_;
209};
210
211template <typename First>
212class SwitchByFirst<First, void> final {
213 public:
214 constexpr explicit SwitchByFirst(First search) noexcept : state_(search) {}
215
216 constexpr SwitchByFirst& Case(First first) noexcept {
217 if (!state_.IsFound() && state_.GetKey() == first) {
218 state_.SetValue(Found{0});
219 }
220 return *this;
221 }
222
223 [[nodiscard]] constexpr bool Extract() noexcept { return state_.IsFound(); }
224
225 private:
226 SearchState<First, Found> state_;
227};
228
229template <typename Second>
230class SwitchByFirstICase final {
231 public:
232 constexpr explicit SwitchByFirstICase(std::string_view search) noexcept
233 : state_(search) {}
234
235 constexpr SwitchByFirstICase& Case(std::string_view first,
236 Second second) noexcept {
237 UASSERT_MSG(!impl::HasUppercaseAscii(first),
238 fmt::format("String literal '{}' in utils::Switch*::Case() "
239 "should be in lower case",
240 first));
241 if (!state_.IsFound() && state_.GetKey().size() == first.size() &&
242 impl::ICaseEqualLowercase(first, state_.GetKey())) {
243 state_.SetValue(second);
244 }
245 return *this;
246 }
247
248 [[nodiscard]] constexpr std::optional<Second> Extract() noexcept {
249 return state_.Extract();
250 }
251
252 private:
253 SearchState<std::string_view, Second> state_;
254};
255
256template <>
257class SwitchByFirstICase<void> final {
258 public:
259 constexpr explicit SwitchByFirstICase(std::string_view search) noexcept
260 : state_(search) {}
261
262 constexpr SwitchByFirstICase& Case(std::string_view first) noexcept {
263 UASSERT_MSG(!impl::HasUppercaseAscii(first),
264 fmt::format("String literal '{}' in utils::Switch*::Case() "
265 "should be in lower case",
266 first));
267 if (!state_.IsFound() && state_.GetKey().size() == first.size() &&
268 impl::ICaseEqualLowercase(first, state_.GetKey())) {
269 state_.SetValue(Found{0});
270 }
271 return *this;
272 }
273
274 [[nodiscard]] constexpr bool Extract() const noexcept {
275 return state_.IsFound();
276 }
277
278 private:
279 SearchState<std::string_view, Found> state_;
280};
281
282template <typename First>
283class SwitchBySecondICase final {
284 public:
285 constexpr explicit SwitchBySecondICase(std::string_view search) noexcept
286 : state_(search) {}
287
288 constexpr SwitchBySecondICase& Case(First first,
289 std::string_view second) noexcept {
290 UASSERT_MSG(!impl::HasUppercaseAscii(second),
291 fmt::format("String literal '{}' in utils::Switch*::Case() "
292 "should be in lower case",
293 second));
294 if (!state_.IsFound() && state_.GetKey().size() == second.size() &&
295 impl::ICaseEqualLowercase(second, state_.GetKey())) {
296 state_.SetValue(first);
297 }
298 return *this;
299 }
300
301 [[nodiscard]] constexpr std::optional<First> Extract() noexcept {
302 return state_.Extract();
303 }
304
305 private:
306 SearchState<std::string_view, First> state_;
307};
308
309template <typename First, typename Second>
310class SwitchBySecond final {
311 public:
312 constexpr explicit SwitchBySecond(Second search) noexcept : state_(search) {}
313
314 constexpr SwitchBySecond& Case(First first, Second second) noexcept {
315 if (!state_.IsFound() && state_.GetKey() == second) {
316 state_.SetValue(first);
317 }
318 return *this;
319 }
320
321 [[nodiscard]] constexpr std::optional<First> Extract() noexcept {
322 return state_.Extract();
323 }
324
325 private:
326 SearchState<Second, First> state_;
327};
328
329template <typename First, typename Second>
330class SwitchTypesDetected final {
331 public:
332 using first_type = First;
333 using second_type = Second;
334
335 constexpr SwitchTypesDetected& Case(First, Second) noexcept { return *this; }
336};
337
338template <typename First>
339class SwitchTypesDetected<First, void> final {
340 public:
341 using first_type = First;
342 using second_type = void;
343
344 constexpr SwitchTypesDetected& Case(First) noexcept { return *this; }
345};
346
347class SwitchTypesDetector final {
348 public:
349 constexpr SwitchTypesDetector& operator()() noexcept { return *this; }
350
351 template <typename First, typename Second>
352 constexpr auto Case(First, Second) noexcept {
353 using first_type =
354 std::conditional_t<std::is_convertible_v<First, std::string_view>,
355 std::string_view, First>;
356 using second_type =
357 std::conditional_t<std::is_convertible_v<Second, std::string_view>,
358 std::string_view, Second>;
359 return SwitchTypesDetected<first_type, second_type>{};
360 }
361
362 template <typename First>
363 constexpr auto Case(First) noexcept {
364 using first_type =
365 std::conditional_t<std::is_convertible_v<First, std::string_view>,
366 std::string_view, First>;
367 return SwitchTypesDetected<first_type, void>{};
368 }
369};
370
371class CaseCounter final {
372 public:
373 template <typename First, typename Second>
374 constexpr CaseCounter& Case(First, Second) noexcept {
375 ++count_;
376 return *this;
377 }
378
379 template <typename First>
380 constexpr CaseCounter& Case(First) noexcept {
381 ++count_;
382 return *this;
383 }
384
385 [[nodiscard]] constexpr std::size_t Extract() const noexcept {
386 return count_;
387 }
388
389 private:
390 std::size_t count_{0};
391};
392
393class CaseDescriber final {
394 public:
395 template <typename First, typename Second>
396 CaseDescriber& Case(First first, Second second) noexcept {
397 if (!description_.empty()) {
398 description_ += ", ";
399 }
400
401 description_ += fmt::format("('{}', '{}')", first, second);
402
403 return *this;
404 }
405
406 [[nodiscard]] std::string Extract() && noexcept {
407 return std::move(description_);
408 }
409
410 private:
411 std::string description_{};
412};
413
414class CaseFirstDescriber final {
415 public:
416 template <typename First>
417 CaseFirstDescriber& Case(First first) noexcept {
418 if (!description_.empty()) {
419 description_ += ", ";
420 }
421
422 description_ += fmt::format("'{}'", first);
423
424 return *this;
425 }
426
427 template <typename First, typename Second>
428 CaseFirstDescriber& Case(First first, Second /*second*/) noexcept {
429 return Case(first);
430 }
431
432 [[nodiscard]] std::string Extract() && noexcept {
433 return std::move(description_);
434 }
435
436 private:
437 std::string description_{};
438};
439
440class CaseSecondDescriber final {
441 public:
442 template <typename First, typename Second>
443 CaseSecondDescriber& Case(First /*first*/, Second second) noexcept {
444 if (!description_.empty()) {
445 description_ += ", ";
446 }
447
448 description_ += fmt::format("'{}'", second);
449
450 return *this;
451 }
452
453 [[nodiscard]] std::string Extract() && noexcept {
454 return std::move(description_);
455 }
456
457 private:
458 std::string description_{};
459};
460
461} // namespace impl
462
463/// @ingroup userver_universal userver_containers
464///
465/// @brief Bidirectional unordered map for trivial types, including string
466/// literals; could be efficiently used as a unordered non-bidirectional map.
467///
468/// @snippet universal/src/utils/trivial_map_test.cpp sample string bimap
469///
470/// utils::TrivialBiMap and utils::TrivialSet are known to outperform
471/// std::unordered_map if:
472/// * there's 32 or less elements in map/set
473/// * or keys are string literals and all of them differ in length.
474///
475/// Implementation of string search is \b very efficient due to
476/// modern compilers optimize it to a switch by input string
477/// length and an integral comparison (rather than a std::memcmp call). In other
478/// words, it usually takes O(1) to find the match in the map.
479///
480/// The same story with integral or enum mappings - compiler optimizes them
481/// into a switch and it usually takes O(1) to find the match.
482///
483/// @snippet universal/src/utils/trivial_map_test.cpp sample bidir bimap
484///
485/// For a single value Case statements see @ref utils::TrivialSet.
486template <typename BuilderFunc>
487class TrivialBiMap final {
488 using TypesPair =
489 std::invoke_result_t<const BuilderFunc&, impl::SwitchTypesDetector>;
490
491 public:
492 using First = typename TypesPair::first_type;
493 using Second = typename TypesPair::second_type;
494
495 /// Returns Second if T is convertible to First, otherwise returns Second
496 /// type.
497 template <class T>
498 using MappedTypeFor =
499 std::conditional_t<std::is_convertible_v<T, First>, Second, First>;
500
501 constexpr TrivialBiMap(BuilderFunc&& func) noexcept : func_(std::move(func)) {
502 static_assert(std::is_empty_v<BuilderFunc>,
503 "Mapping function should not capture variables");
504 static_assert(std::is_trivially_copyable_v<First>,
505 "First type in Case must be trivially copyable");
506 static_assert(!std::is_void_v<Second>,
507 "If second type in Case is missing, use "
508 "utils::TrivialSet instead of utils::TrivialBiMap");
509 static_assert(std::is_trivially_copyable_v<Second>,
510 "Second type in Case must be trivially copyable");
511 }
512
513 constexpr std::optional<Second> TryFindByFirst(First value) const noexcept {
514 return func_(
515 [value]() { return impl::SwitchByFirst<First, Second>{value}; })
516 .Extract();
517 }
518
519 constexpr std::optional<First> TryFindBySecond(Second value) const noexcept {
520 return func_(
521 [value]() { return impl::SwitchBySecond<First, Second>{value}; })
522 .Extract();
523 }
524
525 template <class T>
526 constexpr std::optional<MappedTypeFor<T>> TryFind(T value) const noexcept {
527 static_assert(
528 !std::is_convertible_v<T, First> || !std::is_convertible_v<T, Second>,
529 "Ambiguous conversion, use TryFindByFirst/TryFindBySecond instead");
530
531 if constexpr (std::is_convertible_v<T, First>) {
532 return TryFindByFirst(value);
533 } else {
534 return TryFindBySecond(value);
535 }
536 }
537
538 /// @brief Case insensitive search for value.
539 ///
540 /// For efficiency reasons, first parameter in Case() should be lower case
541 /// string literal.
543 std::string_view value) const noexcept {
544 return func_([value]() { return impl::SwitchByFirstICase<Second>{value}; })
545 .Extract();
546 }
547
548 /// @brief Case insensitive search for value.
549 ///
550 /// For efficiency reasons, second parameter in Case() should be lower case
551 /// string literal.
553 std::string_view value) const noexcept {
554 return func_([value]() { return impl::SwitchBySecondICase<First>{value}; })
555 .Extract();
556 }
557
558 /// @brief Case insensitive search for value that calls either
559 /// TryFindICaseBySecond or TryFindICaseByFirst.
561 std::string_view value) const noexcept {
562 static_assert(!std::is_convertible_v<std::string_view, First> ||
563 !std::is_convertible_v<std::string_view, Second>,
564 "Ambiguous conversion, use "
565 "TryFindICaseByFirst/TryFindICaseBySecond");
566
567 if constexpr (std::is_convertible_v<std::string_view, First>) {
568 return TryFindICaseByFirst(value);
569 } else {
570 return TryFindICaseBySecond(value);
571 }
572 }
573
574 /// Returns count of Case's in mapping
575 constexpr std::size_t size() const noexcept {
576 return func_([]() { return impl::CaseCounter{}; }).Extract();
577 }
578
579 /// Returns a string of comma separated quoted values of Case parameters.
580 ///
581 /// \b Example: "('a', '1'), ('b', '2'), ('c', '3')"
582 ///
583 /// Parameters of Case should be formattable.
584 std::string Describe() const {
585 return func_([]() { return impl::CaseDescriber{}; }).Extract();
586 }
587
588 /// Returns a string of comma separated quoted values of first Case
589 /// parameters.
590 ///
591 /// \b Example: "'a', 'b', 'c'"
592 ///
593 /// First parameters of Case should be formattable.
594 std::string DescribeFirst() const {
595 return func_([]() { return impl::CaseFirstDescriber{}; }).Extract();
596 }
597
598 /// Returns a string of comma separated quoted values of second Case
599 /// parameters.
600 ///
601 /// \b Example: "'1', '2', '3'"
602 ///
603 /// Second parameters of Case should be formattable.
604 std::string DescribeSecond() const {
605 return func_([]() { return impl::CaseSecondDescriber{}; }).Extract();
606 }
607
608 /// Returns a string of comma separated quoted values of Case
609 /// parameters that matches by type.
610 ///
611 /// \b Example: "'1', '2', '3'"
612 ///
613 /// Corresponding Case must be formattable
614 template <typename T>
615 std::string DescribeByType() const {
616 if constexpr (std::is_convertible_v<T, First>) {
617 return DescribeFirst();
618 } else {
619 return DescribeSecond();
620 }
621 }
622
623 private:
624 const BuilderFunc func_;
625};
626
627template <typename BuilderFunc>
628TrivialBiMap(BuilderFunc) -> TrivialBiMap<BuilderFunc>;
629
630/// @ingroup userver_universal userver_containers
631///
632/// @brief Unordered set for trivial types, including string literals.
633///
634/// For a two-value Case statements or efficiency notes
635/// see @ref utils::TrivialBimap.
636template <typename BuilderFunc>
637class TrivialSet final {
638 using TypesPair =
639 std::invoke_result_t<const BuilderFunc&, impl::SwitchTypesDetector>;
640
641 public:
642 using First = typename TypesPair::first_type;
643 using Second = typename TypesPair::second_type;
644
645 constexpr TrivialSet(BuilderFunc&& func) noexcept : func_(std::move(func)) {
646 static_assert(std::is_empty_v<BuilderFunc>,
647 "Mapping function should not capture variables");
648 static_assert(std::is_trivially_copyable_v<First>,
649 "First type in Case must be trivially copyable");
650 static_assert(std::is_void_v<Second>,
651 "Second type in Case should be skipped in utils::TrivialSet");
652 }
653
654 constexpr bool Contains(First value) const noexcept {
655 return func_(
656 [value]() { return impl::SwitchByFirst<First, Second>{value}; })
657 .Extract();
658 }
659
660 constexpr bool ContainsICase(std::string_view value) const noexcept {
661 static_assert(std::is_convertible_v<First, std::string_view>,
662 "ContainsICase works only with std::string_view");
663
664 return func_([value]() { return impl::SwitchByFirstICase<void>{value}; })
665 .Extract();
666 }
667
668 constexpr std::size_t size() const noexcept {
669 return func_([]() { return impl::CaseCounter{}; }).Extract();
670 }
671
672 /// Returns a string of comma separated quoted values of Case parameters.
673 ///
674 /// \b Example: "'a', 'b', 'c'"
675 ///
676 /// Parameters of Case should be formattable.
677 std::string Describe() const {
678 return func_([]() { return impl::CaseFirstDescriber{}; }).Extract();
679 }
680
681 private:
682 const BuilderFunc func_;
683};
684
685template <typename BuilderFunc>
686TrivialSet(BuilderFunc) -> TrivialSet<BuilderFunc>;
687
688/// @brief Parses and returns whatever is specified by `map` from a
689/// `formats::*::Value`.
690/// @throws ExceptionType or `Value::Exception` by default, if `value` is not a
691/// string, or if `value` is not contained in `map`.
692/// @see @ref scripts/docs/en/userver/formats.md
693template <typename ExceptionType = void, typename Value, typename BuilderFunc>
694auto ParseFromValueString(const Value& value, TrivialBiMap<BuilderFunc> map) {
695 if constexpr (!std::is_void_v<ExceptionType>) {
696 if (!value.IsString()) {
697 throw ExceptionType(fmt::format(
698 "Invalid value at '{}': expected a string", value.GetPath()));
699 }
700 }
701
702 const auto string = value.template As<std::string>();
703 const auto parsed = map.TryFind(string);
704 if (parsed) return *parsed;
705
706 using Exception =
707 std::conditional_t<std::is_void_v<ExceptionType>,
708 typename Value::Exception, ExceptionType>;
709
710 throw Exception(fmt::format(
711 "Invalid value of {} at '{}': '{}' is not one of {}",
712 compiler::GetTypeName<std::decay_t<decltype(*parsed)>>(), value.GetPath(),
713 string, map.template DescribeByType<std::string>()));
714}
715
716namespace impl {
717
718// @brief Converts `value` to `std::string_view` using `map`. If `value` is not
719// contained in `map`, then crashes the service in Debug builds, or throws
720// utils::InvariantError in Release builds.
721template <typename Enum, typename BuilderFunc>
722std::string_view EnumToStringView(Enum value, TrivialBiMap<BuilderFunc> map) {
723 static_assert(std::is_enum_v<Enum>);
724 if (const auto string = map.TryFind(value)) return *string;
725
727 false,
728 fmt::format("Invalid value of enum {}: {}", compiler::GetTypeName<Enum>(),
729 static_cast<std::underlying_type_t<Enum>>(value)));
730}
731
732} // namespace impl
733
734} // namespace utils
735
736USERVER_NAMESPACE_END