pimpl без динамической локации, как прикольные точки кастомизации.unordered_map. И при этом она немного чудная, потому что ее размер всегда один байт, сколько бы данных мы туда не вставляли. Ну и на самый напоследок бонус -- большая загадка про shared pointer. Так погнали!Something, и у него в приватной секции находилось что-то типа std::mutex, то есть что-то, что нельзя ни копировать, ни перемещать.Something тоже нельзя копировать, нельзя перемещать. Нет ни конструктора, ни move-саймона, вообще ничего нету. И даже нет обычных публичных конструкторов, всё там в приватной секции. Этот класс Something можно было создать только через фабричный метод Create, и на этом все.1#include <mutex>
2#include <vector>
3
4namespace third_party_lib {
5 class Something {
6 public:
7 static Something Create(std::size_t value);
8
9 // ...
10
11 private:
12 std::mutex mutex_;
13 };
14}
std::list от этих Something, назвали переменную clients, и этих клиентов мы создаем там 42 штучки в цикле:1class Component {
2public:
3 Component() {
4 constexpr std::size_t kClientsCount = 42;
5 for (std::size_t i = 0; i < kClientsCount; ++i) {
6 clients.emplace_back(
7 third_party_lib::Something::Create(i);
8 );
9 }
10 }
11
12 // ...
13
14private:
15 std::list<third_party_lib::Something> clients_;
16};
Create, и она как бы вроде как должна работать, но почему-то не работает? А ответ тут заключается в том, что библиотека рассчитана на C++17, и все C++17 есть правила, которые называются guaranteed copy elision.1error: no matching function for call to
2'construct_at(third_party_lib::Something*&, third_party_lib::Something)'
3 518 | std::construct_at(__p, std::forward<_Args>(__args)...);
4 | ~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Something:1Something Create(); // prvalue
Create с emplace_back, emplace_back принимает что-то по правой ссылке, и у этого что-то теперь есть имя. Объект перестает быть prvalue.emplace_back есть код, где там вызывается placement new оператор, но уже провоцированную, но вызывается конструктор Something от v, и в этом месте компилятор-то и скажет, что мы не правы:1auto list::emplace_back(Something&& v) { // не prvalue
2 // ...
3 new (&node.value) Something(v); // v - не prvalue, Something(v) - prvalue
4 // ...
5}
Something на шаблонный аргумент, как это сделано в стандартной библиотеке (все ближе и ближе к тому, что в стандартной библиотеке), то есть в сигнатуре функции emplace_back, то ничего не поменяется, и v все еще не prvalue, Something от v потребует наличие конструктора:1template <typename Arg>
2auto list::emplace_back(Arg&& v) { // не prvalue
3 // ...
4 new (&node.value) Something(v); // v - не prvalue, Something(v) - prvalue
5 // ...
6}
LazyPrvalue:emplace_back.1namespace utils {
2 template <typename Func>
3 class LazyPrvalue final {
4 public:
5 constexpr explicit LazyPrvalue(Func&& func) : func_(std::move(func)) {}
6
7 LazyPrvalue(LazyPrvalue&&) = delete;
8 LazyPrvalue& operator=(LazyPrvalue&&) = delete;
9
10 constexpr /* implitic */ operator std::invoke_result<Func&&>() && {
11 return std::move(func_)();
12 }
13
14 private:
15 Func func_;
16 };
17}
Something, вместо v используем LazyPrvalue:LazyPrvalue в emplace_back, оно идет через все внутренности emplace_back, доходит до того места, где пытаются создать объект Something от v, вызывается оператор неявного преобразования, он возвращает prvalue, конструктор от prvalue звать не надо, и Something материализовался прямо в node. Красота!1template <typename Arg>
2auto list::emplace_back(LazyPrvalue&& v) { // не prvalue
3 // ...
4 new (&node.value) Something(v); // v - не prvalue, Something(v) - prvalue
5 // ...
6}
LazyPrvalue можно превратить в компилирующийся код. Добавляем в LazyPrvalue внутрь в конструктор Something::Create , и готово, все работает:1class Component {
2public:
3 Component() {
4 constexpr std::size_t kClientsCount = 42;
5 for (std::size_t i = 0; i < kClientsCount; ++i) {
6 clients.emplace_back(
7 utils::LazyPrvalue([i]() {
8 return third_party_lib::Something::Create(i);
9 })
10 );
11 }
12 }
13
14 // ...
15private:
16 std::list<third_party_lib::Something> clients_;
17};
LazyPrvalue, но спустя какое-то время мы столкнулись с еще неприятной библиотекой. Тоже какая-то библиотека, в ней есть тоже класс Some, и у него тоже есть что-то наподобие мьютекса. У этого класса уже, правда, наконец-таки есть нормальный конструктор, и с ним тоже надо работать.1#include <mutex>
2#include <vector>
3
4namespace third_party_lib {
5
6class Some {
7public:
8 explicit Some(std::size_t value);
9
10 // ...
11
12private:
13 std::mutex mutex_;
14};
15
16}
1explicit Some(std::size_t value);
std::list от клиентов. В этот раз на рантайме нам на вход передается количество клиентов, которые надо создать, количество инстансов этого класса Some, которые надо держать в списке. И каждый из инстансов Some мы инициализируем какой-то там константой 100500, и все работает, все компилируется, все хорошо.1class Component {
2public:
3
4 Component(std::size_t count) {
5 for (std::size_t i = 0; i < count; ++i) {
6 clients.emplace_back(100500);
7 }
8 }
9
10 // ...
11private:
12 std::list<third_party_lib::Some> clients_;
13};
std::list — не самый оптимальный класс стандартной библиотеки. Он хранит элементы, раскиданные хаотично в динамической памяти. Это не дружелюбно к кэшам процессора. Кэшу процессора приятнее, если все элементы находятся рядом. Также у листа есть недостаток в том, что мы не можем за константное время обратиться к i-му элементу клиента, а нам это может быть очень нужно, например, для балансирования нагрузки между этими клиентами.1private: 2 std::list<third_party_lib::Some> clients_;
std::vector.1private: 2 std::vector<third_party_lib::Some> clients_;
std::list на std::vector, компилятор опять начнет ругаться, что "извини, человек, ты хочешь конструктор позвать, а move-конструктора-то у типа нет".1error: static assertion failed: result type must be constructible from input type 2 90 | static_assert(is_constructible<_ValueType, _Tp>::value, 3 | ^~~~~
push_back, все хорошо, размещаются в этом куске памяти элементы. Но когда мы попытаемся в вектор вставить пятый элемент, он в этот кусок памяти не поместится. Вектору нужно проаллоцировать новый большой кусок памяти и из старого куска памяти переместить элементы в новый кусок памяти, или скопировать, в зависимости от разных факторов. Собственно, в этом месте вектору и нужен move-конструктор или copy-конструктор, а у нас класс Some его не имеет. Плохо.std::unique_ptr от массива заданного на этапе компиляции размера. Тут проблема в том, что размер задан на этапе компиляции, а мы его не знаем. А еще у std::unique_ptr есть неприятность — нет поэлементной инициализации. Класс Some инициализируется с помощью аргумента 100500, а у нас нет возможности этот 100500 в конструктор std::unique_ptr подставить. Плохо.1std::unique<ptr<T[N]>
std::unique_ptr от массива размером, известным на рантайме. Становится чуть получше, но не сильно. Такой std::unique_ptr не помнит свой размер, а мы бы хотели по клиентам итерироваться, получать количество этих клиентов. И его где-то надо будет их прикапывать. И все та же проблема — нет поэлементной инициализации.1std::unique<ptr<T[]>
FixedArray. Он очень простой, у него там наборы typedef-ов, дефолтный конструктор и все те функции, что есть у вектора: для работы с элементами, для получения размера. В приватной секции указатель на данные и количество этих данных.1template <class T>
2class FixedArray final {
3 public:
4 using iterator = T*;
5 using const_iterator = const T*;
6
7 FixedArray() = default;
8
9 /// Make an array and initialize each element with "args"
10 template <class... Args>
11 explicit FixedArray(std::size_t size, Args&... args);
12
13 FixedArray(FixedArray&& other) noexcept;
14 FixedArray& operator=(FixedArray&& other) noexcept;
15
16 FixedArray(const FixedArray& other) = delete;
17 FixedArray& operator=(const FixedArray& other) = delete;
18
19 ~FixedArray();
20
21 std::size_t size() const noexcept { return size_; }
22 bool empty() const noexcept { return size_ == 0; }
23 const T& operator[](std::size_t i) const noexcept;
24
25 // ...
26 private:
27 T* storage{nullptr};
28 std::size_t size_{0};
29};
1template <class T>
2template <class... Args>
3FixedArray<T>::FixedArray(std::size_t size, Args&&... args) : size_(size)
4{
5 if (size_ == 0) return;
6 storage_ = std::allocator<T>{}.allocate(size_);
7 auto* begin = data();
8 try {
9 for (auto* end = begin + size - 1; begin != end; ++begin) {
10 new (begin) T(args...);
11 }
12 new (begin) T(std::forward<Args>(args)...);
13 } catch (...) {
14 std::destroy(data(), begin);
15 std::allocator<T>{}.deallocate(storage_, size);
16 throw;
17 }
18}
1storage_ = std::allocator<T>{}.allocate(size_);
1for (auto* end = begin + size - 1; begin != end; ++begin) { 2 new (begin) T(args...); 3}
FixedArray, мы эту временную строчку переместим в последний элемент. На первых элементах так делать нельзя, потому что после перемещения строчка будет пустой, и мы не тем проинициализируем все остальные элементы, кроме первого.1new (begin) T(std::forward<Args>(args)...);
1} catch (...) { 2 std::destroy(data(), begin); 3 std::allocator<T>{}.deallocate(storage_, size); 4 throw; 5}
FixedArray мы можем заменить std::list на новый тип данных, и внезапно код становится короче, код становится быстрее. У нас появляется константный доступ, у нас меньше памяти тратится, работает со всякими экзотическими типами, у которых нет перемещения, forward-ит аргументы.1class Component {
2public:
3
4 Component(std::size_t count)
5 : clients_(count, 100500)
6 {}
7
8 // ...
9private:
10 utils::FixedArray<third_party_lib::Some> clients_;
11};
GenerateFixedArray, передать на вход количество элементов и лямбду. Лямбда будет возвращать результат вызова Something::Create. Create возвращает prvalue, этот prvalue возвращается из лямбды, это все еще prvalue, он попадает в то место, где мы вызываем placement new, и конструктор вызывать не надо, все значение сразу материализовалось по месту.1class Component {
2public:
3
4 Component(std::size_t count)
5 : clients_(utils::GenerateFixedArray(count, [](std::size_t i) {
6 return third_party_lib::Something::Create(i);
7 }))
8 {}
9
10 // ...
11private:
12 utils::FixedArray<third_party_lib::Something> clients_;
13};
BufferWriter, что она делает? Она шаблонная, она принимает шаблонный класс T, какой-то value, и из этого шаблонного класса через трейты выводит тип форматтера. То есть это некий сериализатор — как этот value сериализовать и передать по проводам.1template <typename T> 2typename traits::IO<T>::FormatterType BufferWriter(const T& value) { 3 using Formatter = typename traits::IO<T>::FormatterType; 4 return Formatter(value): 5}
MyCodegeneratedStructure, и кодогенерация сгенерила заголовочный файл generated.hpp:1struct MyCodegeneratedStructure { /*...*/ };
generated.hpp, то следующая кодогенерация эту сериализацию потрёт, и ничего хорошего не получится. Надо написать эту сериализацию в отдельном файлике my_writer.hpp. Там пользователь делает forward-декларацию для структуры MyCodegeneratedStructure, пишет эту сериализацию, специализирует шаблоны.1struct MyCodegeneratedStructure;
2template <>
3struct storages::postgres::io::CppToUserPg<MyCodegeneratedStructure> {
4 // ...
5};
a.cpp подключает generate.hpp, подключает my_writer.hpp и пишет бизнес-логику. И бизнес-логика работает, всё замечательно.1#include <generated.hpp> 2#include <my_writer.hpp>
b.cpp и забывает подключить хедер-файл my_writer.hpp, где указано, какой сериализацией пользоваться.1#include <generated.hpp>
BufferWriter. И при всём при этом шаблонный аргумент один и тот же, а значит, если имя у функции одно, шаблонный параметр один и тот же, то для линкера эти функции одинаковые, несмотря на то, что у них разные тела.1template <typename T> 2typename traits::IO<T>::FormatterType BufferWriter(const T& value) { 3 using Formatter = typename traits::IO<T>::FormatterType; 4 return Formatter(value): 5}
cpp-файл с правильными инклюдами, а линкер взял и сломался в совершенно стороннем месте. И кажется, что это кто-то там другой тесты неправильно написал, но нет, это линкер хулиганит. При этом ошибку невероятно сложно диагностировать, потому что транзитивные инклюды вы из-за специфики C++ не отследите, где там пришел нужный сериализатор, не пришёл нужный сериализатор, воспользовались мы им где-то заранее или он там где-то в инклюдах, не воспользовались первый раз.a.cpp и всё, что там происходит, он может провалидировать, и там вроде всё в порядке. Есть файлик b.cpp, всё, что там происходит, там тоже всё в порядке. Проблема возникает, когда эти два файлика сливаются в бинарник, то есть это видит только линкер. В принципе есть разные сторонние тулзы, которые пытаются поймать эту проблему. Загвоздка в том, что они её не ловят, особенно если функция маленькая, они чего-то пасуют.CheckForBufferWriterODR, и он принимает два шаблонных параметра: тип, тот который пришёл на вход в эту функцию, и тип форматтера, который мы получили из трейтов, из всего вот этого вот. И вызывается функция RequireInstance.1template <typename T> 2typename traits::IO<T>::FormatterType BufferWriter(const T& value) { 3 using Formatter = typename traits::IO<T>::FormatterType; 4#ifndef NDEBUG 5 detail::CheckForBufferWriterODR<T, Formatter>::content.RequireInstance(); 6#endif 7 return Formatter(value): 8}
1#ifndef NDEBUG
2class WritersRegistrator final {
3 public:
4 WritersRegistrator(std::type_index type, std::type_index formatter_type, const char* base_file);
5 void RequireInstance() const;
6};
7
8namespace{
9template <class Type, class Writer>
10struct CheckForBufferWriterODR final {
11 static inline WritersRegistrator content{typeid(Type), typeid(Writer), __BASE_FILE__};
12};
13} // namespace
14
15#endif
CheckForBufferWriterODR, у него внутри находится статическая инлайн-переменная. То есть как только начинается использование переменной content, как только кто-то пишет CheckForBufferWriterODR и подставляет туда два типа, компилятор эту статическую инлайн-переменную в единице трансляции инстанцирует, помещает её туда, и от неё будет зваться конструктор.writer-а, форматтера, а ещё он принимает супер-пупер волшебный макрос, которым практически никто не пользуется. Макрос называется __BASE_FILE__. Как ни странно, он есть в большинстве современных компиляторов, и он раскрывается в имя cpp-файла, из которого был подключен вот этот хедер. Да, тот код, что видите, это всё в хедере.1namespace{
2template <class Type, class Writer>
3struct CheckForBufferWriterODR final {
4 static inline WritersRegistrator content{typeid(Type), typeid(Writer), __BASE_FILE__};
5};
6} // namespace
Type и Writer могут совпадать в разных единицах трансляции, но __BASE_FILE__ точно будет там разный. Мы из одного cpp-файлика, из другого cpp-файлика, шаблонные аргументы одни и те же, __BASE_FILE__ разный. Чтобы линкер лишнее не выкинул, мы в хедер-файле всё это оборачиваем в анонимный неймспейс, что является анти-паттерном, и поэтому мы эту штуку используем только в дебаге, чтобы у нас в релизе бинарники в размере не увеличивались. С анонимным спейсом линкер перестаёт думать, что эти типы одинаковые, они в анонимном спейсе и перестаёт их выкидывать.1namespace{
2template <class Type, class Writer>
3struct CheckForBufferWriterODR final {
4 static inline WritersRegistrator content{typeid(Type), typeid(Writer), __BASE_FILE__};
5};
6} // namespace
WritersRegistrator — это та статическая инлайн-переменная, у которой вызывается конструктор. Он принимает typeid типа, typeid форматтера, base_file, и внутри себя содержит какую-нибудь std::unordered_map от типа и форматтера, с которыми этот тип использовался, и base_file.1class WritersRegistrator final {
2 public:
3 WritersRegistrator(std::type_index type, std::type_index formatter_type, const char* base_file);
4 void RequireInstance() const;
5};
RequireInstance мы можем в дебаге пройтись по этой std::unordered_map и посмотреть, что у каждого типа только один форматтер.1template <typename T> 2typename traits::IO<T>::FormatterType BufferWriter(const T& value) { 3 using Formatter = typename traits::IO<T>::FormatterType; 4#ifndef NDEBUG 5 detail::CheckForBufferWriterODR<T, Formatter>::content.RequireInstance(); 6#endif 7 return Formatter(value): 8}
MyCodegeneratedStructure имеет разные инстанцирования форматтеров. Есть дефолтный форматтер, есть пользовательский форматтер, и дефолтный форматтер располагается в файлике b.cpp, а пользовательский форматтер в файлике a.cpp.1Type 'MyCodegeneratedStructure' has conflicting instantiation 2of formatters: 'pg::DefaultFormatter' vs 3'storages::postgres::io::CppToUserPg<MyCodegeneratedStructure>' in base 4files [b.cpp] vs [a.cpp]
std::unordered_map показался нам медленным, хотелось сильно быстрее. Ну а чтобы это была не просто оптимизация ради оптимизации, мы ещё решили докинуть функциональных требований.std::unordered_map, а из enum в строку надо где-то отдельно описывать. Неприятно. Хочется один раз задать маппинг и чтобы он умел в одну сторону конвертировать и в другую сторону. То есть нужна бимапа. Также нам хотелось иметь возможность получить все значения из enum-а, получить все значения строк, что очень удобно, когда вы даёте пользователю диагностику. Например, можно будет выдавать диагностику: "Пользователь, ты ввел значение строки foo, а у нас enum понимает только значение bar и baz". И всё, и разработчику понятно, что он не передал что-то не так, и легко понять, где именно он передал что-то не так.1template <class Key, class Value, std::size_t N>
2class ConstinitMap {
3 public:
4 constinit explicit ConstinitMap(std::pair<Key, Value>(&&map)[N]) {
5 CompileTimeSlowSort(map);
6 for (std::size_t i = 0; i < N; ++i) {
7 keys_[i] = map[i].first;
8 values_[i] = map[i].second;
9 }
10 CompileTimeAssertUnique(keys_);
11 }
12
13 constexpr bool Contains(const Key& key) const noexcept;
14 constexpr const Value* FindOrNullptr(const Key& key) const noexcept;
15
16 // ...
17
18 private:
19 Key keys_[N] = {};
20 Value values_[N] = {};
21};
1private: 2 Key keys_[N] = {}; 3 Value values_[N] = {};
constinit, то есть конструктор обязан выполниться на этапе компиляции. И всё, что в нем, выполняется на этапе компиляции, поэтому тут можно взять и медленно — пузырьком — посортировать элементы, которые нам пришли, после чего спокойно взять эти элементы, скопировать массив ключей, массив значений, на compile-time проверить, что все ключи уникальны, а если это не так, выдать на compile-time ошибку, прервать пользователю компиляцию, и сказать, что "что-то ты не то задал".std::unordered_map. Классно, Классно прям! Взяли, похвастались об этом в чатике procxx, и достаточно быстро нам прилетел коммент: "А посмотрите на llvm::StringSwitch".1constinit explicit ConstinitMap(std::pair<Key, Value>(&&map)[N]) {
2 CompileTimeSlowSort(map);
3 for (std::size_t i = 0; i < N; ++i) {
4 keys_[i] = map[i].first;
5 values_[i] = map[i].second;
6 }
7 CompileTimeAssertUnique(keys_);
8}
x — это runtime-значение строки, то есть цвет какой-то. Дальше через .Case, через вызовы функций, описывается маппинг строки на enum. Если пришла строка red, тогда enum Red. Если пришла строка orange, тогда enum нужно вернуть Orange. Напоследок дефолтное значение можно вернуть, которое вернётся, если мы ничего не нашли.1enum Color { 2 Red, Orange, Yellow, UnknownColor, 3}; 4 5Color color = llvm::StringSwitch<Color>(x) 6 .Case("red", Red) 7 .Case("orange", Orange) 8 .Case("yellow", Yellow) 9 .Default(UnknownColor);
llvm::StringSwitch создаёт внутри себя пустой std::optional, а каждый кейс преобразовывается в блок if, где мы проверяем, что если std::optional пустой, тогда сравниваем x с той строкой, которая была в кейсе. Если сравнение говорит, что true, заходим внутрь этого if и в std::optional записываем то значение, которое было вторым аргументом в кейсе. Повторяем, повторяем, и наконец, дефолтное значение — если в std::optional пусто, возвращаем дефолт.1std::optional<Color> color;
2if (!color && x == "red") {
3 color = Red;
4}
5if (!color && x == "orange") {
6 color = Orange;
7}
8if (!color && x == "yellow") {
9 color = Yellow;
10}
11
12return color.value_or(UnknownColor);
TrivialBiMap. Инициализируется она лямбдой, которая принимает селектор, и на этом селекторе у нас вызываются все эти кейсы.1constexpr utils::TrivialBiMap kMyEnumDescription3 = [](auto selector) { 2 return selector() 3 .Case("a", 9) 4 .Case("ab", 10) 5 .Case("abc", 11) 6 .Case("abcd", 12) 7 .Case("abcde", 13) 8 .Case("abcdef", 14) 9 .Case("abcdefg", 15) 10 .Case("abcdefgz", 16) 11 .Case("abcdefgzx", 17) 12 .Case("abcdefgzxz", 18) 13}; 14 15int StringCase3(std::string_view param) { 16 return *kMyEnumDescription3.TryFind(param); 17}
std::unordered_map. Даже на самых вырожденных случаях, которые явно должны плохо себя вести на этой тривиальной бимапе, где там 50 значений, все они одинаковой длины. TrivialBiMap все равно чуть-чуть быстрее, чем std::unordered_map. Нереально круто!1Benchmark Time CPU Iterations 2---------------------------------------------------------------------- 3MappingSmallTrivialBiMap 12.2 ns 12.0 ns 57766546 4MappingSmallUnordered 167 ns 164 ns 3844217 5 6MappingMediumTrivialBiMap 19.5 ns 19.1 ns 33153861 7MappingMediumUnordered 210 ns 207 ns 3130815 8 9MappingHugeTrivialBiMap 72.2 ns 71.0 ns 9917775 10MappingHugeUnordered 264 ns 264 ns 2584118 11 12MappingHugeTrivialBiMapLast 19.1 ns 19.0 ns 37326680 13MappingHugeUnorderedLast 23.0 ns 22.6 ns 30076471
1std::optional<Color> color;
2if (!color && x == "red") {
3 color = Red;
4}
5if (!color && x == "orange") {
6 color = Orange;
7}
8if (!color && x == "yellow") {
9 color = Yellow;
10}
11
12return color.value_or(UnknownColor);
std::optional что-то записали, это значение уже дальше внутри if-ов мы не заходим, значение сразу возвращается. И, соответственно, он соптимизировал вот этот код до вот такого.1if (x == "red") {
2 return Red;
3}
4if (x == "orange") {
5 return Orange;
6}
7if (x == "yellow") {
8 return Yellow
9}
10
11return UnknownColor;
x — это std::string, либо std::string_view, и сравнение std::string_view с литералом — с массивом чаров — это сравнение их размеров, а потом посимвольное сравнение.1if (x.size() == 3 && x[0] == 'r' && x[1] == 'e' && x[2] == 'd') { 2 return Red; 3} 4if (x.size() == 6 && x[0] == 'o' && x[1] == 'r' && x[2] == 'a' && x[3] == 'n' && x[4] == 'g' && x[5] == 'e') { 5 return Orange; 6} 7if (x.size() == 6 && x[0] == 'y' && x[1] == 'e' && x[2] == 'l' && x[3] == 'l' && x[4] == 'o' && x[5] == 'w') { 8 return Yellow 9} 10 11return UnknownColor;
if везде одна и та же, чуть-чуть различается константа. Компилятор берёт и одинаковое выражение выносит из if, превращая выражение вот в такое. У нас есть switch по размеру, а дальше ветки кейсов по размеру пришедшей на вход строки. Другими словами, компилятор взял и сделал нам unordered-мапу, где в качестве хэша выступает длина строки. Вместо линейного поиска мы получили бац и хитрую unordered-мапу, которая даже хэш не вычисляет, а сразу переходит в нужную длину строки.jump — это как раз switch по длине строки. Казалось бы, круто! Вообще офигенно! Но на этом не всё, чудеса не заканчиваются. Оба компилятора сравнивают строчку с жутко длинными непонятными константами. Почему так?1switch (x.size()) { 2case 3: 3 return (x[0] == 'r' && x[1] == 'e' && x[2] == 'd') ? Red : UnknownColor; 4 5case 6: 6 if (x[0] == 'o' && x[1] == 'r' && x[2] == 'a' && x[3] ...) { 7 return Orange; 8 } 9 if (x[0] == 'y' && x[1] == 'e' && x[2] == 'l' && x[3] ...) { 10 return Yellow; 11 } 12} 13return UnknownColor;
x длина такая-то, там три символа или шесть символов. Если мы делаем посимвольное сравнение, это сделать равенство, потом битовое И, ну или логическое И, потом опять сравнение, потом опять логическая операция — дофига кода. Но можно же не посимвольно сравнивать.1// todo 2switch (x.size()) { 3case 3: 4 return (x[0] == 'r' && x[1] == 'e' && x[2] == 'd') ? Red : UnknownColor; 5 6case 6: 7 if (x[0] == 'o' && x[1] == 'r' && x[2] == 'a' && x[3] ...) { 8 return Orange; 9 } 10 if (x[0] == 'y' && x[1] == 'e' && x[2] == 'l' && x[3] ...) { 11 return Yellow; 12 } 13} 14return UnknownColor;
1switch (x.size()) { 2case 3: 3 return (x == 12731231283121) ? Red : UnknownColor; 4 5case 6: 6 if (x == 123123123123123123) { 7 return Orange; 8 } 9 if (x == 789789798789789789) { 10 return Yellow; 11 } 12} 13return UnknownColor;
std::unordered_map раза так в два-три. То есть ещё и размер бинарника становится меньше.constexpr. Всё, что с этой unordered-мапой происходит, можно делать на compile-time, и также мы получили case-sensitive сравнение строк. Записали в unordered-мапе в нижнем регистре — можно сравнивать пришедшие строки в произвольном регистре, компилятор там тоже понаоптимизирует.TrivialBiMap принимает на вход функцию, вот та вот здоровенная generic-лямбда, которая была на слайде, вот он её берет и сохраняет в приватной секции.1template <typename BuilderFunc>
2class TrivialBiMap final {
3 using TypesPair = std::invoke_result_t<const BuilderFunc&, impl::SwitchTypesDetector>;
4
5 public:
6 using First = typename TypesPair::first_type;
7 using Second = typename TypesPair::second_type;
8
9 constexpr TrivialBiMap(BuilderFunc func) noexcept;
10
11 constexpr std::optional<Second> TryFindByFirst(First value) const noexcept {
12 return func_(impl::SwitchByFirst<First, Second>{value}).Extract();
13 }
14
15 private:
16 const BuilderFunc func_;
17};
1private: 2 const BuilderFunc func_;
.Case.1constexpr std::optional<Second> TryFindByFirst(First value) const noexcept {
2 return func_(impl::SwitchByFirst<First, Second>{value}).Extract();
3}
1template <typename First, typename Second>
2class SwitchByFirst final {
3 public:
4 constexpr explicit SwitchByFirst(First search) noexcept : search_(search) {}
5
6 constexpr SwitchByFirst& Case(First first, Second second) noexcept {
7 if (!result_ && search_ == first) {
8 result_.emplace(second);
9 }
10 return *this;
11 }
12
13 [[nodiscard]] constexpr std::optional<Second> Extract() noexcept {
14 return result_;
15 }
16
17 private:
18 const First search_;
19 std::optional<Second> result_{};
20};
llvm::StringSwitch, где в начале у нас есть какой-то std::optional, мы его проверяем, а потом проверяем рантаймовое значение строки с тем, что у нас записано в кейсе.1constexpr SwitchByFirst& Case(First first, Second second) noexcept {
2 if (!result_ && search_ == first) {
3 result_.emplace(second);
4 }
5 return *this;
6}
SwitchByFirst.1constexpr utils::TrivialBiMap kMyEnumDescription3 = [](auto selector) { 2 return selector() 3 .Case("a", 9) 4 .Case("ab", 10) 5 .Case("abc", 11) 6 .Case("abcd", 12) 7 .Case("abcde", 13) 8 .Case("abcdef", 14) 9 .Case("abcdefg", 15) 10 .Case("abcdefgz", 16) 11 .Case("abcdefgzx", 17) 12 .Case("abcdefgzxz", 18) 13}; 14 15int StringCase3(std::string_view param) { 16 return *kMyEnumDescription3.TryFind(param); 17}
char, два указателя. Они, скорее всего, будут не равны. То есть нужно то, что приходит в Case, ещё преобразовывать в std::string_view. Также вам нужно преобразовывать эти литералы в std::string_view, потому что иначе компилятор теряет информацию о размере и перестаёт сравнивать ваши литералы — runtime-строчку и литерал на вход — по размеру первым шагом, и unordered-мапы не происходит. Так что аккуратность нужна, нужно явно вывести типы First и Second, которые попадают в этот Case в визиторах. И вычисляемые эти типы тоже используют ту generic лямбда-функцию, которая нам пришла на вход.SwitchTypesDetector, который уже возвращает некий тип, из которого можно достать First и Second.1 using TypesPair = std::invoke_result_t<const BuilderFunc&, impl::SwitchTypesDetector>; 2 3public: 4 using First = typename TypesPair::first_type; 5 using Second = typename TypesPair::second_type;
SwitchTypesDetector реализовать, как ни странно, весьма просто.1template <typename First, typename Second>
2struct SwitchTypesDetected final {
3 using first_type = First;
4 using second_type = Second;
5 constexpr SwitchTypesDetected& Case(First, Second) noexcept { return *this; }
6};
7
8struct SwitchTypesDetector final {
9 template <typename First, typename Second>
10 constexpr auto Case(First, Second) noexcept {
11 using first_type = std::conditional_t<std::is_convertible_v<First, std::string_view>, std::string_view, First>;
12 using second_type = std::conditional_t<std::is_convertible_v<Second, std::string_view>, std::string_view, Second>;
13 return SwitchTypesDetected<first_type, second_type>{};
14 }
15};
Case, он принимает First, принимает Second, и это всё, что его интересует. Его не интересуют эти значения, его интересуют только типы.1struct SwitchTypesDetector final {
2 template <typename First, typename Second>
3 constexpr auto Case(First, Second) noexcept {
First можно преобразовать к std::string_view, то тип First у нас будет std::string_view. Нельзя преобразовать к std::string_view — кто-то передал значение enum — значит, First-тип у нас — это значение enum.1using first_type = std::conditional_t<std::is_convertible_v<First, std::string_view>, std::string_view, First>;
Second-типом то же самое: если что-то нам передали, что превращается в строчку, возвращаем std::string_view. Если нет, то возвращаем enum. Таким образом, можно делать даже преобразование из одной строки в другую строку или из одного enum в другой enum.1using second_type = std::conditional_t<std::is_convertible_v<Second, std::string_view>, std::string_view, Second>;
conditional-ы и возвращаем другой тип данных, уже SwitchTypesDetected. То есть все последующие кейсы, кроме первого, вызываются на вот этом SwitchTypesDetected.1return SwitchTypesDetected<first_type, second_type>{};
SwitchTypesDetected очень простой: у него Case вообще ничего не делает. Всё, что делает этот SwitchTypesDetected, он хранит в себе first_type, second_type, готово. Мы получили реальную хитрую шуструю бимапу.1template <typename First, typename Second>
2struct SwitchTypesDetected final {
3 using first_type = First;
4 using second_type = Second;
5 constexpr SwitchTypesDetected& Case(First, Second) noexcept { return *this; }
6};
1return std::shared_ptr<Logger>( 2 std::shared_ptr<void>{}, 3 &GetNullLogger() 4);
std::shared_ptr. У меня заняло больше времени понять, что здесь происходит, да. Смотрите, что здесь происходит, и зачем это вообще нужно.std::shared_ptr. При этом это не простой конструктор std::shared_ptr от сырого указателя, а так называемый aliasing-конструктор std::shared_ptr. Для чего он нужен? Вот есть у вас std::shared_ptr на какую-то структуру, и вам очень нужен std::shared_ptr на int, который находится в этой структуре. Если вы возьмете просто указатель на этот int, то вообще ничего не понятно, как это сделать. Вам нужно взять из std::shared_ptr на структуру счетчик, который счетчик использований, и сделать так, чтобы новый std::shared_ptr, который указывает на int, переиспользовал счетчик от того std::shared_ptr. Если старый std::shared_ptr умрет, то структура все еще жива, её не удалили, и она будет жить, пока жив и второй std::shared_ptr. То есть aliasing-конструктор — это возможность сделать std::shared_ptr разных типов, которые при этом используют один и тот же reference counter.std::shared_ptr, из которого он достается reference counter, а потом передается указатель на ту переменную, которой std::shared_ptr владеет. То, что он возвращает, то, чем он владеет. Загвоздка тут в том, что первым параметром в конструктор std::shared_ptr был передан std::shared_ptr от void, дефолтно сконструированный, то есть он ничего не проаллоцировал, у него нет счетчика ссылок вообще.std::shared_ptr, у которого нет счетчика ссылок, он при копированиях ничего не инкрементит, в деструкторах ничего не декрементит, но при этом его value — это то, что было передано вторым параметром. И таким std::shared_ptr можно пользоваться. Только на самом деле это не std::shared_ptr, это сырой указатель, который притворяется std::shared_ptr. Зачем?std::shared_ptr, и этот std::shared_ptr мы куда-то внутрь засовываем, чтобы потом этим логгером пользоваться. И там ещё куча у нас всяких хитростей, чтобы счётчики не инкрементить, когда std::shared_ptr действительно кто-то пользуется. Об этом в следующий раз как-нибудь расскажу.NullLogger — логгер, который ничего не делает. Ему данные передают, а он ничего не делает. И такой логгер-то динамически аллоцировать не нужно. Достаточно одного экземпляра одного этого логгера на всех, потому что он весь константный, ничего не содержит, ничего не делает, в общем. Но нужен std::shared_ptr на него. И вот эта конструкция, что здесь на экране, это позволяет вам сделать std::shared_ptr на NullLogger, при этом std::shared_ptr лайфтаймом этого NullLogger не владеет, и вообще это фактически сырой указатель, который притворяется std::shared_ptr, который можно передавать в разные части API, и будет хорошо.1Logger& GetNullLogger() noexcept {
2 static NullLogger null_logger{};
3 return null_logger;
4}
5
6std::shared_ptr<Logger> MakeNullLogger() {
7 return std::shared_ptr<Logger>(
8 std::shared_ptr<void>{},
9 &GetNullLogger()
10 );
11}
std::shared_ptr невладеющий. Если у него взять std::weak_ptr, то он будет сразу expired. То есть вот такая особенность. А так вопросов нет.static_vector, если известен верхний предел размера? Если нет, то может стоит законтрибьютить такого рода фиксированный вектор?static_vector — да, поможет. Но вам придется тогда комбинировать его с LazyPrvalue, если вы захотите передавать туда какие-нибудь хитрые объекты, у которых нет никаких конструкторов, через emplace_back. В противном случае, да, можно. Кстати, мы FixedArray внезапно начали использовать в огромном количестве мест. Как только он у нас появился, мы поняли, что мы хотим создавать там массивы атомиков, массивы специальных наших собственных мьютексов. И прям можно поискать по кодовой базе, много где используется.
Boost мы в проекте стараемся в публичные хедер-файлы особо не выставлять, потому что он тащит огромное количество других библиотек Boost, зависимости, и замедляет время компиляции. Хорошая новость: в конце этого года мы все исправим, и Boost отбрасывает обратную совместимость с C++03. То есть с этого конца этого года только C++11, и соответственно, можно не заморачиваться, не использовать бустовые static_assert, type_traits, все библиотеки Boost, которые перестанут заморачиваться, станут значительно легче для компиляции. Как бы 10 лет прошло – кажется, пора. Точнее, 20 лет с C++03. Вот теперь точно пора.std::string_view и там будет my::string_view или boost_string_view? Сумеет ли это компилятор оптимизировать?std::string_view. Насколько я помню, boost::string_view в последних версиях Boost конвертируется в std::string_view. Поэтому да, оно будет работать. Просто boost::string_view, который передадите, он сконвертируется в std::string_view, за константное время сконвертируется, все готово, без динамических локаций. Все работает.std::string_view физически отсутствует.string_view. Главное, чтобы он сравнивался в начале по size с другим string_view, и тогда компилятору станет хорошо и приятно.boost::hana::map?FixedArray там есть, вот эти бимапы там есть, корутины, драйвера асинхронные к базам данных, stackful-корутины, примитивы синхронизации на корутинах. В общем, всё, всё, всё. Потихоньку мы хорошие наработки из фреймворка что-то перетаскиваем в стандартную библиотеку, что-то перетаскиваем в Boost, что-то просто говорим комитету: "Смотрите, у нас вот так вот это хорошо получилось". Комитет говорит: "О, мы вот сейчас подшаманим, будет ещё лучше". Прогресс движется.std::shared_ptr, когда мы выставляем новый логгер, то есть нам тут требуется именно std::shared_ptr от логгера. Можно сделать std::shared_ptr от логгера через динамическую аллокацию и создания NullLogger-а свеженького, но нам как бы одного достаточно. Зачем динамически что-то аллоцировать, если одного логгера хватит всем? Вот и всё.std::shared_ptr с недавнего времени не копируем, не инкрементим, когда делается логирование, и при этом оно асинхронное и там много всего интересного, страшного происходит, смачного. Вот как-то так.int. Когда начали формулировать задачу, сразу подумал, что я бы сделал это на двух switch-ах, это даже можно сделать на С++03. Первый switch был бы просто int -> return строка. А для второго я бы использовал horner hash, то есть посчитал бы в compile time хэш от строк, которые нужны. А в рантайме я бы считал один раз хэш от строки и сравнивал его с ключами. Тут получился бы как бы switch от строк, которого штатно нет, но можно его сэмулировать. Вы же на вход получаете строчку, которая известна в рантайме. Всё равно нужно её номер узнать, чтобы сравнить её с остальными. Всё равно как будто бы считается хэш.инт-у, вторая то, что вы говорите, аналог. Но проблема в том, что это две функции, там два раза этот маппинг надо писать. Не хорошо. А еще из двух функций мы не вытащим все значения строк и все значения enum. А нам хотелось и в одну стороно конвертировать, и в другую сторону конвертировать, и все значения получать, и size получать, и при этом написать всё один раз, чтобы случайно не ошибиться или при рефакторинге, когда новые значения enum-а появляются, чтобы мы не забыли его добавить сразу во все места. Если один маппинг, всё просто — в одно место добавили, всё работает. Со switch-ами подход жизнеспособный, но здесь функциональность сильно удобнее становится. Просто меньше кода значительно и меньше шанс ошибки, и функционал больше, и диагностика приятнее.
По поводу того, что хэш происходит в самом начале. Вместо хэша выступает размер строки. Компилятор делает вот это преобразование, и здесь в качестве хэша выступает размер строки. Его считать не надо, он пришел от runtime-строки, и там прям размер в переменной хранится, его не надо считать.x взять и получить из этого всего длинный int. Это фактически есть как horner hash. Именно это он и делает.TrivialBiMap, он изначально ориентирован на то, что конвертируется строка в enum, enum в строку, строка в строку, и эти строки где-то используются. Это параметры на вход, либо это что-то конфигурационно понятное, а не что-то сгенерированное машиной для машины. И соответственно, там строчка имеет понятное читаемое имя. Или, например, мы используем TrivialBiMap где-то там при логированиях. У нас есть специальные теги, которые при логировании нельзя использовать, например, имя модуля или thread id. И если пользователь их задаёт, мы через TrivialBiMap смотрим, есть такое, нету. Если пользователь задал значение, значит, не надо так делать, и на рантайме выдаем ошибку. Быстрый поиск.