userver: userver/utils/trivial_map.hpp Source File
Loading...
Searching...
No Matches
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