Addressof
#опытным
Говорят вот, что питон - такой легкий для входа в него язык. Его код можно читать, как английские английский текст. А вот С/С++ хаят за его несколько отталкивающую внешность. Чего только указатели стоят...
Кстати о них. Все мы знаем, как получить адрес объекта:
Человек, ни разу не видевший код на плюсах, увидит здесь какие-то магические символы. Вроде число, а вроде какие-то руны * и &. Но плюсы тоже могут в читаемость! Причем именно в аспекте адресов.
Вместо непонятного новичкам амперсанда есть функция std::addressof! Она шаблонная, позволяет получить реальный адрес непосредственно самого объекта и доступна с С++11. Для нее кстати удалена перегрузка с const T&&
Это делает функцию еще одним примером использования константной правой ссылки .
Это конечно круто, что можно в плюсах словами брать адрес, но в чем прикол? Зачем было заводить отдельную функцию для того, что уже есть в самом языке?
А вот теперь мы возвращаемся к предыдущему посту про перегрузку оператора взятия адреса. Так как его можно перегружать, то мы можем возвращать вообще любой адрес, который потенциально никак не связан с самим объектом. В этом случае не очень понятно, как взять трушный адрес объекта. Как раз таки std::addressof - способ получить валидный адрес непосредственно самого объекта.
Также большим преимуществом является шаблонная природа функции. Это позволяет обобщенному коду работать, как с обычными классами, так и с классами, у которых перегружен оператор взятия адреса.
А с С++17 она еще и констэкспр, это для любителей компайл-тайма.
Вот вам примерчик:
Здесь какие-то злые персоналии перегрузили оператор взятия адреса у класса Ptr так, чтобы он возвращал указатель на одно из его полей. Ну и потом сравнивают результат работы оператора с результатом выполнения функции std::addressof.
Видно, что трушный адрес объекта, полученный с помощью this и адрес, возвращенный из std::addressof полностью совпадают. А перегруженный оператор возвращает другое значение.
Express your thoughts clearly. Stay cool.
#cpp #cpp11 #cpp17
#опытным
Говорят вот, что питон - такой легкий для входа в него язык. Его код можно читать, как английские английский текст. А вот С/С++ хаят за его несколько отталкивающую внешность. Чего только указатели стоят...
Кстати о них. Все мы знаем, как получить адрес объекта:
int number = 42;
int * p_num = &number;
Человек, ни разу не видевший код на плюсах, увидит здесь какие-то магические символы. Вроде число, а вроде какие-то руны * и &. Но плюсы тоже могут в читаемость! Причем именно в аспекте адресов.
Вместо непонятного новичкам амперсанда есть функция std::addressof! Она шаблонная, позволяет получить реальный адрес непосредственно самого объекта и доступна с С++11. Для нее кстати удалена перегрузка с const T&&
template< class T >
T* addressof( T& arg ) noexcept;
template< class T >
const T* addressof( const T&& ) = delete;
Это делает функцию еще одним примером использования константной правой ссылки .
Это конечно круто, что можно в плюсах словами брать адрес, но в чем прикол? Зачем было заводить отдельную функцию для того, что уже есть в самом языке?
А вот теперь мы возвращаемся к предыдущему посту про перегрузку оператора взятия адреса. Так как его можно перегружать, то мы можем возвращать вообще любой адрес, который потенциально никак не связан с самим объектом. В этом случае не очень понятно, как взять трушный адрес объекта. Как раз таки std::addressof - способ получить валидный адрес непосредственно самого объекта.
Также большим преимуществом является шаблонная природа функции. Это позволяет обобщенному коду работать, как с обычными классами, так и с классами, у которых перегружен оператор взятия адреса.
А с С++17 она еще и констэкспр, это для любителей компайл-тайма.
Вот вам примерчик:
template<class T>
struct Ptr
{
T* pad; // add pad to show difference between 'this' and 'data'
T* data;
Ptr(T* arg) : pad(nullptr), data(arg)
{
std::cout << "Ctor this = " << this << '\n';
}
~Ptr() { delete data; }
T** operator&() { return &data; }
};
template<class T>
void f(Ptr<T>* p)
{
std::cout << "Ptr overload called with p = " << p << '\n';
}
void f(int** p)
{
std::cout << "int** overload called with p = " << p << '\n';
}
int main()
{
Ptr<int> p(new int(42));
f(&p); // calls int** overload
f(std::addressof(p)); // calls Ptr<int>* overload, (= this)
}
// OUTPUT
// Ctor this = 0x7fff59ae6e88
// int** overload called with p = 0x7fff59ae6e90
// Ptr overload called with p = 0x7fff59ae6e88
Здесь какие-то злые персоналии перегрузили оператор взятия адреса у класса Ptr так, чтобы он возвращал указатель на одно из его полей. Ну и потом сравнивают результат работы оператора с результатом выполнения функции std::addressof.
Видно, что трушный адрес объекта, полученный с помощью this и адрес, возвращенный из std::addressof полностью совпадают. А перегруженный оператор возвращает другое значение.
Express your thoughts clearly. Stay cool.
#cpp #cpp11 #cpp17
Double lookup
#опытным
Решил сделать небольшое дополнение к предыдущему посту по результатам дискуссии в комментариях.
Не нужно использовать методы count(key) и contains(key), если вы потом собираетесь работать с этим ключом в ассоциативном контейнере(например изменять объект по ключу). Это может привести к так называемому double lookup. То есть двойной поиск по контейнеру.
Возьмем для примера std::map для показательности. И вот такой код:
Здесь мы по ключу key выдаем какое-то значение value. Эти значения вычисляются при помощи долгой функции longCalculations, поэтому было решено закэшировать все уже вычисленные значения в мапе. Так мы сможем обойти долгие вычисления и быстро дать ответ в случае, если уже искали это значение.
Только вот в чем проблема. Поиск по мапе - логарифмическая по времени операция. И в этом примере мы всегда делаем 2 поиска: первый раз на contains(мы должны пройтись по контейнеру, чтобы понять, есть ли элемент) и второй раз на operator[](нужно пройтись по контейнеру, чтобы вставить в него элемент/получить к нему доступ). Долговато как-то. Может можно получше?
В случае, если ключ есть в мапе, то мы можем делать всего 1 поиск! С помощью метода find и итератора на элемент.
Мы в начале попытались найти конкретный элемент мапы по ключу. И если его нет, то нам все-таки придется выполнить второй поиск, чтобы найти подходящее место для элемента. Но вот если ключ есть, тогда мы можем использовать сам итератор для возврата значения и второго поиска не будет!
Методы count и contains нужно использовать только тогда, когда у вас нет надобности в получении доступа к элементам контейнера. Find в этом случае, по моему мнению, немного синтаксически избыточен. А вот говорящие методы - самая тема. Например, у вас есть множество, в котором хранятся какие-то поля джейсона. И вам нужно как-то трансформировать только те значения, ключи которых находятся во множестве.
Все прекрасно читается и никакого двойного поиска!
Don't do the same work twice. Stay cool.
#cppcore #goodpractice
#опытным
Решил сделать небольшое дополнение к предыдущему посту по результатам дискуссии в комментариях.
Не нужно использовать методы count(key) и contains(key), если вы потом собираетесь работать с этим ключом в ассоциативном контейнере(например изменять объект по ключу). Это может привести к так называемому double lookup. То есть двойной поиск по контейнеру.
Возьмем для примера std::map для показательности. И вот такой код:
std::map<std::string, std::string> map;
std::string get_value(const std::string& key) {
if (!map.contains(key)) {
std::string value = longCalculations(key);
map[key] = value;
return value;
} else {
return map[key];
}
}
Здесь мы по ключу key выдаем какое-то значение value. Эти значения вычисляются при помощи долгой функции longCalculations, поэтому было решено закэшировать все уже вычисленные значения в мапе. Так мы сможем обойти долгие вычисления и быстро дать ответ в случае, если уже искали это значение.
Только вот в чем проблема. Поиск по мапе - логарифмическая по времени операция. И в этом примере мы всегда делаем 2 поиска: первый раз на contains(мы должны пройтись по контейнеру, чтобы понять, есть ли элемент) и второй раз на operator[](нужно пройтись по контейнеру, чтобы вставить в него элемент/получить к нему доступ). Долговато как-то. Может можно получше?
В случае, если ключ есть в мапе, то мы можем делать всего 1 поиск! С помощью метода find и итератора на элемент.
std::string get_value(const std::string& key) {
auto it = map.find(key);
if (it == map.end()) {
std::string value = longCalculations(key);
map[key] = value;
return value;
} else {
return it->second;
}
}
Мы в начале попытались найти конкретный элемент мапы по ключу. И если его нет, то нам все-таки придется выполнить второй поиск, чтобы найти подходящее место для элемента. Но вот если ключ есть, тогда мы можем использовать сам итератор для возврата значения и второго поиска не будет!
Методы count и contains нужно использовать только тогда, когда у вас нет надобности в получении доступа к элементам контейнера. Find в этом случае, по моему мнению, немного синтаксически избыточен. А вот говорящие методы - самая тема. Например, у вас есть множество, в котором хранятся какие-то поля джейсона. И вам нужно как-то трансформировать только те значения, ключи которых находятся во множестве.
std::set<std::string> tokens;
std::string json_token;
Json json;
if (tokens.contains(json_token)) {
transformJson(json, json_token);
}
Все прекрасно читается и никакого двойного поиска!
Don't do the same work twice. Stay cool.
#cppcore #goodpractice
Почему приватные методы находятся в описании класса?
#опытным
Публичные, защищенные методы - понятно, они нужны либо для пользователей класса, либо для наследников. Поля - понятно, они влияют на размер объекта, а клиенты должны уметь правильно аллоцировать нужное количество памяти под объект. Ну а приватные-то методы зачем раскрывать?
Приватные методы - это вообще-то детали реализации. Если я в своем фреймворке по-приколу назову какой-нибудь приватный метод KillTheNigga, то другим людям уже нельзя будет пользоваться этим фреймворком. Закенселлят еще меня. Хотя какая блин разница, какие у меня там приватные методы? Они типа и названы private, чтобы показать, что они МОИ ЛИЧНЫЕ(никому не отдам). А их оказывается нужно еще и показывать всем. Что-то не сходится.
Ну во-первых. Представим, что этого ограничения нет. Тогда все приватные методы объявлялись и определялись бы в файле реализации, который никто не видит. Но если я могу в файле реализации определить приватный метод, то кто угодно может это сделать. Это будет давать рандомным людям прямой доступ к закрытым полям класса. Если мы завершили определение класса, то у нас нет способов как-то пометить именно наши файлы, как "благословленные владельцем". Есть всего лишь юниты трансляции и они равнозначны. Получается, что единственный способ сказать, что вот этот набор методов официально одобрен создателем - это объявить его в описании класса.
На самом деле, в С++ мы имеем прямой доступ к памяти, в значит, мы легко можем поменять байтики для приватных полей и все. Или даже создать тип, с таким же описанием, только с дополнительным методом. Кастануть недоступный тип к своему и вуаля, вы можете как хотите вертеть объектом во всех удобных вам позах. Но это уже хаки, мы такого не одобряем. Не используя манипуляции с памятью, мы не сможем добавлять рандомную функциональность в рандомный класс.
А во-вторых, оказывается в С++ приватные методы участвуют в разрешении перегрузок(внезапно). В целом, так оно и должно быть. Никто не мешает вам определить публичный метод и перегрузить его приватным методом. Проблема(или фича) в том, что этап разрешения перегрузок стоит перед проверкой модификатора доступа. То есть даже если метод приватный и его никто не должен увидеть, он все равно участвует в разрешении перегрузок наряду со всеми остальными методами. Поэтому каждый клиент должен видеть полный набор приватных методов. Об этом мы уже говорили в контексте pimpl идиомы.
В чем прикол такого дизайн решения?
Давайте представим, что такого правила нет. И вот у нас есть две перегрузки, одна приватная для double, другая публичная для int. И перегрузка с double всегда отбрасывалась бы только лишь по причине того, что она приватная. Тогда мы легко можем вызвать публичную функцию с дробным числом 1.5 и нам ничего не будет. Оно просто неявно приведется к int и все на этом.
А теперь посмотрим, что будет, если мы поменяем модификатор приватной перегрузки на публичный? Ничего не упадет, НО! наш вызов метода с аргументом 1.5 теперь пойдет в другую функцию! То есть изменится поведение объекта. Комитет хотел избежать таких ситуаций, поэтому ввели такое вот ограничение. Наверное, причина не одна. Но это весомый аргумент.
Однако, такой протокол поведения влечет за собой различные сайд-эффекты. Я могу удалить(=delete) приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации.
Stay aware. Stay cool.
#design #howitworks #cppcore #hardcore
#опытным
Публичные, защищенные методы - понятно, они нужны либо для пользователей класса, либо для наследников. Поля - понятно, они влияют на размер объекта, а клиенты должны уметь правильно аллоцировать нужное количество памяти под объект. Ну а приватные-то методы зачем раскрывать?
Приватные методы - это вообще-то детали реализации. Если я в своем фреймворке по-приколу назову какой-нибудь приватный метод KillTheNigga, то другим людям уже нельзя будет пользоваться этим фреймворком. Закенселлят еще меня. Хотя какая блин разница, какие у меня там приватные методы? Они типа и названы private, чтобы показать, что они МОИ ЛИЧНЫЕ(никому не отдам). А их оказывается нужно еще и показывать всем. Что-то не сходится.
Ну во-первых. Представим, что этого ограничения нет. Тогда все приватные методы объявлялись и определялись бы в файле реализации, который никто не видит. Но если я могу в файле реализации определить приватный метод, то кто угодно может это сделать. Это будет давать рандомным людям прямой доступ к закрытым полям класса. Если мы завершили определение класса, то у нас нет способов как-то пометить именно наши файлы, как "благословленные владельцем". Есть всего лишь юниты трансляции и они равнозначны. Получается, что единственный способ сказать, что вот этот набор методов официально одобрен создателем - это объявить его в описании класса.
На самом деле, в С++ мы имеем прямой доступ к памяти, в значит, мы легко можем поменять байтики для приватных полей и все. Или даже создать тип, с таким же описанием, только с дополнительным методом. Кастануть недоступный тип к своему и вуаля, вы можете как хотите вертеть объектом во всех удобных вам позах. Но это уже хаки, мы такого не одобряем. Не используя манипуляции с памятью, мы не сможем добавлять рандомную функциональность в рандомный класс.
А во-вторых, оказывается в С++ приватные методы участвуют в разрешении перегрузок(внезапно). В целом, так оно и должно быть. Никто не мешает вам определить публичный метод и перегрузить его приватным методом. Проблема(или фича) в том, что этап разрешения перегрузок стоит перед проверкой модификатора доступа. То есть даже если метод приватный и его никто не должен увидеть, он все равно участвует в разрешении перегрузок наряду со всеми остальными методами. Поэтому каждый клиент должен видеть полный набор приватных методов. Об этом мы уже говорили в контексте pimpl идиомы.
В чем прикол такого дизайн решения?
Давайте представим, что такого правила нет. И вот у нас есть две перегрузки, одна приватная для double, другая публичная для int. И перегрузка с double всегда отбрасывалась бы только лишь по причине того, что она приватная. Тогда мы легко можем вызвать публичную функцию с дробным числом 1.5 и нам ничего не будет. Оно просто неявно приведется к int и все на этом.
А теперь посмотрим, что будет, если мы поменяем модификатор приватной перегрузки на публичный? Ничего не упадет, НО! наш вызов метода с аргументом 1.5 теперь пойдет в другую функцию! То есть изменится поведение объекта. Комитет хотел избежать таких ситуаций, поэтому ввели такое вот ограничение. Наверное, причина не одна. Но это весомый аргумент.
Однако, такой протокол поведения влечет за собой различные сайд-эффекты. Я могу удалить(=delete) приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации.
Stay aware. Stay cool.
#design #howitworks #cppcore #hardcore
std::apply
#опытным
Метапрогеры очень любят работать с компайл-тайм структурами, типа std::array, std::pair и std::tuple. Когда работают с такими структурами, то интересны прежде всего элементы этих структур. И очень хочется иногда как-то единообразно передавать их распакованные элементы куда-то в другую функцию.
Именно этим и занимается std::apply, которая появилась в С++17. По факту, эта такое дженерик решение для того, чтобы вызвать какую-то функцию с аргументами из элементов tuple-like объектов.
Простейшее, что можно с ней делать - вывести на экран все элементы тапла.
Здесь мы применяем fold-expression и оператор-запятая. Можете освежить знания в этом посте.
Можно придумать чуть более сложную задачу. Надо написать функцию, которая принимает неограниченное число параметров, в том числе и tuple-like объекты. Все параметры надо распаковать в строку, а tuple-like объекты выделить с помощью фигурных скобок. Объекты естественно могут быть вложенные.
Может получится что-то такое:
Вывод будет такой:
Даже не знаю, как эту лапшу разбирать. Идея такая что is_tuple_like_v проверяет аргумент на соответствие tuple-like объекту. Если нам на очередном вызове serialize_tuple_like попался такой объект, то мы берем и распаковываем его параметры в рекурсивный вызов serialize_tuple_like. Если у нас не tuple-like объект, то просто выводим его в стрим. Наверное, нужны проверки на то, что объект можно вывести в стрим, но решил, что это немного борщ для этого кода.
У функции довольно специфичные кейсы использования. Впрочем, как и у всей метапроги.
Don't live in metaverse. Stay cool.
#template #cpp17
#опытным
Метапрогеры очень любят работать с компайл-тайм структурами, типа std::array, std::pair и std::tuple. Когда работают с такими структурами, то интересны прежде всего элементы этих структур. И очень хочется иногда как-то единообразно передавать их распакованные элементы куда-то в другую функцию.
Именно этим и занимается std::apply, которая появилась в С++17. По факту, эта такое дженерик решение для того, чтобы вызвать какую-то функцию с аргументами из элементов tuple-like объектов.
Простейшее, что можно с ней делать - вывести на экран все элементы тапла.
const std::tuple<int, char> tuple = std::make_tuple(5, 'a');
std::apply([](const auto&... elem)
{
((std::cout << elem << ' '), ..., (std::cout << std::endl));
}, tuple);
Здесь мы применяем fold-expression и оператор-запятая. Можете освежить знания в этом посте.
Можно придумать чуть более сложную задачу. Надо написать функцию, которая принимает неограниченное число параметров, в том числе и tuple-like объекты. Все параметры надо распаковать в строку, а tuple-like объекты выделить с помощью фигурных скобок. Объекты естественно могут быть вложенные.
Может получится что-то такое:
template <typename T, typename = void>
struct is_tuple_like : std::false_type {};
template <typename T>
struct is_tuple_like<T, std::void_t<decltype(std::tuple_size<T>::value), decltype(std::get<0>(std::declval<T>()))>> : std::true_type {};
template <typename T>
constexpr bool is_tuple_like_v = is_tuple_like<T>::value;
template<typename Tval, typename ... T>
void serialize_tuple_like(std::stringstream &outbuf, const Tval& arg, const T& ... rest) noexcept {
if constexpr (is_tuple_like_v<Tval>){
outbuf << "{ ";
std::apply([&outbuf](auto const&... packed_values) {
serialize_tuple_like(outbuf, packed_values ...);
}, arg);
outbuf << " }";
}
else{
outbuf << arg;
}
if constexpr(sizeof...(rest) > 0){
outbuf << ' ';
serialize_tuple_like(outbuf, rest ...);
}
}
template<typename ... T>
std::string args_to_string(const T& ... args) noexcept {
std::stringstream outbuf{};
if constexpr(sizeof...(args) > 0){
serialize_tuple_like(outbuf, args ...);
}
return outbuf.str();
}
int main(){
std::cout << args_to_string("test", 1,
std::tuple{"tuple1", 2, 3.0,
std::tuple{"tuple2", "boom"}},
std::pair{"pair", 4},
std::array{5, 6, 7, 8, 9});
}
Вывод будет такой:
test 1 { tuple1 2 3 { tuple2 boom } } { pair 4 } { 5 6 7 8 9 }
Даже не знаю, как эту лапшу разбирать. Идея такая что is_tuple_like_v проверяет аргумент на соответствие tuple-like объекту. Если нам на очередном вызове serialize_tuple_like попался такой объект, то мы берем и распаковываем его параметры в рекурсивный вызов serialize_tuple_like. Если у нас не tuple-like объект, то просто выводим его в стрим. Наверное, нужны проверки на то, что объект можно вывести в стрим, но решил, что это немного борщ для этого кода.
У функции довольно специфичные кейсы использования. Впрочем, как и у всей метапроги.
Don't live in metaverse. Stay cool.
#template #cpp17
std::visit
#опытным
Не так уж и просто работать с вариантными типами. Надо точно знать, какого типа объект находится внутри. Если не угадали - получили исключение. Ну или тестить объект на содержание в нем конкретного типа с помощью лапши из if-else.
Так вот чтобы голова не болела при работе с std::variant надо 2 раза в день после еды принимать std::visit.
Эта функция позволяет применять функтор к одному или нескольким объектам std::variant. И самое главное, что вам не нужно беспокоиться по поводу того, какой именно объект находится за личиной варианта. Компилятор все сам сделает.
Так выглядят ее сигнатуры. Первым параметром передаем функтор, дальше идут варианты.
Попробуем использовать эту функцию:
Главное, чтобы функтор умел обрабатывать любую комбинацию типов, которую вы можете передать в него. Обратите внимание, что мы используем здесь generic лямбду, которая может принимать один аргумент любого типа.
Если вы хотите передать в std::visit несколько объектов, то функтор должен принимать ровно такое же количество аргументов и уметь обрабатывать любую комбинацию типов, которая может содержаться в вариантах.
Используем здесь дженерик вариадик лямбду, чтобы она могла принимать столько аргументов, сколько нам нужно. И эта конструкция работает для любого количества переданных объектов std::variant;
Так что std::variant и std::visit - закадычные друзья и им друг без друга грустно! Не заставляйте их грустить.
Have a trustworthy helper. Stay cool.
#template #cpp17
#опытным
Не так уж и просто работать с вариантными типами. Надо точно знать, какого типа объект находится внутри. Если не угадали - получили исключение. Ну или тестить объект на содержание в нем конкретного типа с помощью лапши из if-else.
Так вот чтобы голова не болела при работе с std::variant надо 2 раза в день после еды принимать std::visit.
Эта функция позволяет применять функтор к одному или нескольким объектам std::variant. И самое главное, что вам не нужно беспокоиться по поводу того, какой именно объект находится за личиной варианта. Компилятор все сам сделает.
template< class Visitor, class... Variants >
constexpr visit( Visitor&& vis, Variants&&... vars );
template< class R, class Visitor, class... Variants >
constexpr R visit( Visitor&& vis, Variants&&... vars );
Так выглядят ее сигнатуры. Первым параметром передаем функтор, дальше идут варианты.
Попробуем использовать эту функцию:
using var_t = std::variant<int, long, double, std::string>;
std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
for (auto& v: vec)
{
var_t w = std::visit([](auto&& arg) -> var_t { return arg + arg; }, v);
std::visit([](auto&& arg){ std::cout << arg; }, w);
}
//OUTPUT:
// 20 30 3 hellohello
Главное, чтобы функтор умел обрабатывать любую комбинацию типов, которую вы можете передать в него. Обратите внимание, что мы используем здесь generic лямбду, которая может принимать один аргумент любого типа.
Если вы хотите передать в std::visit несколько объектов, то функтор должен принимать ровно такое же количество аргументов и уметь обрабатывать любую комбинацию типов, которая может содержаться в вариантах.
std::visit([](auto&&... arg){ ((std::cout << arg << " "),
...,
(std::cout << std::endl)); }, vec[0], vec[1]);
std::visit([](auto&&... arg){ ((std::cout << arg << " "),
...,
(std::cout << std::endl)); }, vec[0], vec[1], vec[2]);
// OUTPUT
// 10 15
// 10 15 1.5
Используем здесь дженерик вариадик лямбду, чтобы она могла принимать столько аргументов, сколько нам нужно. И эта конструкция работает для любого количества переданных объектов std::variant;
Так что std::variant и std::visit - закадычные друзья и им друг без друга грустно! Не заставляйте их грустить.
Have a trustworthy helper. Stay cool.
#template #cpp17
Доступ к элементам многомерных структур
#опытным
Если вы спросите разработчиков C++ о том, как они получают доступ к элементам многомерных массивов, скорее всего, вы получите несколько различных ответов в зависимости от их опыта.
Если вы спросите кого-то, кто не очень опытен или работает в нематематической области, есть большая вероятность, что ответ будет таким, что вы должны использовать несколько операторов[] подряд: myMatrix[x][y].
Есть несколько проблем с таким подходом:
⛔️ Это не очень удобно чисто внешне. Все номальные люди используют синтаксис [x, y].
⛔️ Это работает "из коробки" на реально многомерных структурах, типа вложенных массивов(типа вектора векторов). Чтобы поддержать даже такой синтаксис для кастомных классов, придется несколько приседать.
⛔️ Поэтому многие находят лазейки, чтобы делать что-то похожее на [x, y], этих лазеек много, нет какого-то стандарта.
⛔️ Стандарт использует operator[] с одним аргументом для получения доступа к элементам массивов.
⛔️ Лазейки неконсистентны с одноразмерными массивами в плане получения доступа к элементам.
⛔️ Некоторые из них преполагают спорную семантику, а некоторые делают практически нечитаемыми сообщения об ошибках компиляции.
⛔️ Возможные проблемы с инлайнингом.
Рассмотрим лазейки в будущем, а сейчас сфокусируемся на решении проблемы.
В С++23 наконец завезли многоаргументный operator[]. Теперь при проектировании своей матрицы или даже тензора перегружать оператор[] для 1, 2, 3 и более входных аргументов. Так для матрицы можно возвращать элемент, если мы передали 2 размерности, или возвращать всю строку, если мы передали только одну размерность.
Здесь мы создали матрицу 4х3, заполнили ее буквами алфавита и вывели на экран каждый элемент через matrix[x, y]. А также дальше получили целую строку матрицы через matrix[x] и вывели ее содержимое на экран:
В качестве обертки для строки используем std::span из С++20.
Очень красиво и удобно. Разработчикам математических библиотек сделали большой подарок.
Be consistent. Stay cool.
#cpp23 #cpp20
#опытным
Если вы спросите разработчиков C++ о том, как они получают доступ к элементам многомерных массивов, скорее всего, вы получите несколько различных ответов в зависимости от их опыта.
Если вы спросите кого-то, кто не очень опытен или работает в нематематической области, есть большая вероятность, что ответ будет таким, что вы должны использовать несколько операторов[] подряд: myMatrix[x][y].
Есть несколько проблем с таким подходом:
⛔️ Это не очень удобно чисто внешне. Все номальные люди используют синтаксис [x, y].
⛔️ Это работает "из коробки" на реально многомерных структурах, типа вложенных массивов(типа вектора векторов). Чтобы поддержать даже такой синтаксис для кастомных классов, придется несколько приседать.
⛔️ Поэтому многие находят лазейки, чтобы делать что-то похожее на [x, y], этих лазеек много, нет какого-то стандарта.
⛔️ Стандарт использует operator[] с одним аргументом для получения доступа к элементам массивов.
⛔️ Лазейки неконсистентны с одноразмерными массивами в плане получения доступа к элементам.
⛔️ Некоторые из них преполагают спорную семантику, а некоторые делают практически нечитаемыми сообщения об ошибках компиляции.
⛔️ Возможные проблемы с инлайнингом.
Рассмотрим лазейки в будущем, а сейчас сфокусируемся на решении проблемы.
В С++23 наконец завезли многоаргументный operator[]. Теперь при проектировании своей матрицы или даже тензора перегружать оператор[] для 1, 2, 3 и более входных аргументов. Так для матрицы можно возвращать элемент, если мы передали 2 размерности, или возвращать всю строку, если мы передали только одну размерность.
template <typename T, std::size_t ROWS, std::size_t COLS>
class Martrix {
std::array<T, ROWS * COLS> a;
public:
Martrix() = default;
Martrix(Martrix const&) = default;
constexpr T& operator[](std::size_t row, std::size_t col) { // C++23 required
assert(row < ROWS and col < COLS);
return a[COLS * row + col];
}
constexpr std::span<T> operator[](std::size_t row) {
assert(row < ROWS);
return std::span{a.data() + row * COLS, COLS};
}
constexpr auto& underlying_array() { return a; }
};
int main() {
constexpr size_t ROWS = 4;
constexpr size_t COLS = 3;
Martrix<char, ROWS, COLS> matrix;
// fill in the underlying 1D array
auto& arr = matrix.underlying_array();
std::iota(arr.begin(), arr.end(), 'A');
for (auto row {0U}; row < ROWS; ++row) {
std::cout << "│ ";
for (auto col {0U}; col < COLS; ++col) {
std::cout << matrix[row, col] << " │ ";
}
std::cout << "\n";
}
std::cout << "\n";
auto row = matrix[1];
for (auto col {0U}; col < COLS; ++col) {
std::cout << row[col] << ' ';
}
}
Здесь мы создали матрицу 4х3, заполнили ее буквами алфавита и вывели на экран каждый элемент через matrix[x, y]. А также дальше получили целую строку матрицы через matrix[x] и вывели ее содержимое на экран:
│ A │ B │ C │
│ D │ E │ F │
│ G │ H │ I │
│ J │ K │ L │
D E F
В качестве обертки для строки используем std::span из С++20.
Очень красиво и удобно. Разработчикам математических библиотек сделали большой подарок.
Be consistent. Stay cool.
#cpp23 #cpp20
std::mdspan
#опытным
"Я понял, что можно перегружать оператор[] для разного числа аргументов. Но это только для моих классов. А что делать со стандартными контейнерами типа std::vector? Могу я как-то на нем использовать многоаргументный оператор, если по факту я храню в нем матрицу?"
И нет, и да.
Интерфейс семантически одномерного контейнера никто менять не будет.
Однако вместе с С++23 появился еще один полезный класс std::mdspan. Это фактически тот же std::span, то есть это view над одномерной последовательностью элементов, только он интерпретирует ее, как многомерный массив.
То есть вы теперь буквально можете интерпретировать свой std::array или std::vector, как многомерный массив.
И! У std::mdspan переопределен оператор[], который может принимать несколько измеренений и выдает ссылку на соответствующий элемент.
Вывод:
В этом примере мы интерпретируем один и тот же массив, как матрицу и как такую кубическую структуру. Ну и играемся с выводом, чтобы продемонстировать, что мы реально можем манипулировать многомерной структурой, как хотим. В начале заполняем массив, как матрицу с двумя строчками(значения в строчках отличаются на 1000). Дальше читаем массив, как 3-хмерную структуру 2х3х2. Разрезаем ее на 2 части и получаются две матрицы 3х2, которые и выводим на экран.
Для создания mdspan нужно передать в конструктор начальный итератор и последовательные размерности. Их может быть сколько угодно. Число элементов или последний элемент последовательности не нужны, так как набор размерностей однозначно задает число элементов.
Метод extend возвращает размер вьюхи по заданному ранк индексу.
Так что скоро можно даже будет обойтись без сооружения своих оберток над стандартными контейнерами для получения доступа к многомерному оператору[]. И использовать стандартный инструмент std::mdspan.
Use standard things. Stay cool.
#cpp23 #STL
#опытным
"Я понял, что можно перегружать оператор[] для разного числа аргументов. Но это только для моих классов. А что делать со стандартными контейнерами типа std::vector? Могу я как-то на нем использовать многоаргументный оператор, если по факту я храню в нем матрицу?"
И нет, и да.
Интерфейс семантически одномерного контейнера никто менять не будет.
Однако вместе с С++23 появился еще один полезный класс std::mdspan. Это фактически тот же std::span, то есть это view над одномерной последовательностью элементов, только он интерпретирует ее, как многомерный массив.
То есть вы теперь буквально можете интерпретировать свой std::array или std::vector, как многомерный массив.
И! У std::mdspan переопределен оператор[], который может принимать несколько измеренений и выдает ссылку на соответствующий элемент.
std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
// View data as contiguous memory representing 2 rows of 6 ints each
auto ms2 = std::mdspan(v.data(), 2, 6);
// View the same data as a 3D array 2 x 3 x 2
auto ms3 = std::mdspan(v.data(), 2, 3, 2);
// Write data using 2D view
for (std::size_t i = 0; i != ms2.extent(0); i++)
for (std::size_t j = 0; j != ms2.extent(1); j++)
ms2[i, j] = i * 1000 + j;
// Read back using 3D view
for (std::size_t i = 0; i != ms3.extent(0); i++)
{
std::println("slice @ i = {}", i);
for (std::size_t j = 0; j != ms3.extent(1); j++)
{
for (std::size_t k = 0; k != ms3.extent(2); k++)
std::print("{} ", ms3[i, j, k]);
std::println("");
}
}
Вывод:
slice @ i = 0
0 1
2 3
4 5
slice @ i = 1
1000 1001
1002 1003
1004 1005
В этом примере мы интерпретируем один и тот же массив, как матрицу и как такую кубическую структуру. Ну и играемся с выводом, чтобы продемонстировать, что мы реально можем манипулировать многомерной структурой, как хотим. В начале заполняем массив, как матрицу с двумя строчками(значения в строчках отличаются на 1000). Дальше читаем массив, как 3-хмерную структуру 2х3х2. Разрезаем ее на 2 части и получаются две матрицы 3х2, которые и выводим на экран.
Для создания mdspan нужно передать в конструктор начальный итератор и последовательные размерности. Их может быть сколько угодно. Число элементов или последний элемент последовательности не нужны, так как набор размерностей однозначно задает число элементов.
Метод extend возвращает размер вьюхи по заданному ранк индексу.
Так что скоро можно даже будет обойтись без сооружения своих оберток над стандартными контейнерами для получения доступа к многомерному оператору[]. И использовать стандартный инструмент std::mdspan.
Use standard things. Stay cool.
#cpp23 #STL
Допотопный доступ к многомерному массиву Ч1
#опытным
Начнем рассказ о том, как люди до С++23(то есть до сих пор) жили с оператором[], принимающим только один параметр.
И начнем мы с банальщины. Вот у нас есть класс матрицы. По всем канонам С++ мы должны получать доступ к ее элементам вот так matrix[i][j]. Этот формат сохраняет констистентность с доступом к элементам одномерных массивов.
Однако сразу натыкаемся на проблему. Класс один, а вызываем мы оператор два раза. Несостыковочка.
Ее решает паттерн прокси. Мы создаем прокси класс и возвращаем его объект из первого индекса. Дальше у этого прокси класса определяем оператор[] и на выходе получаем наш элемент.
Условно, из первого оператора возвращаем ссылку на строку матрицы, а из второго - уже сам элемент.
Выглядит это примерно так:
В примере мы 2 раза проходимся по матрице, чтобы продемонстрировать возможность индексации элементов через объект самой матрицы и объекта строки.
Можно было бы конечно не писать отдельно наш прокси тип ArraySpan, а использовать готовый std::span из С++20, но оставим так. Идея использовать такой легковесный объект понятна - нам не нужно лишнего оверхеда на копирование или создание сложного объекта, чтобы просто получить доступ к элементу матрицы.
Ну и здесь мы можем использовать прокси тип в качестве реальной строки матрицы, и не скрывать его в кишках класса. Так мы получаем доступ к большей вариативности в оперировании матрицами. Например, можно делать скалярное произведение строк и прочее.
В чем недостаток такого способа? Куча прокси классов, которые возможно и не нужны. Возможно, нам просто нужна двумерная структура, чтобы получать доступ к конкретным элементам. Использование карты для морского боя не предполагает использование отдельных строк или столбцов. Хотелось бы просто индексировать конкретные элементы. Но тем не менее мы вынуждены использовать прокси класс.
А если структура трухмерная? Уже 2 прокси нужно будет. Больше вложенность - больше классов-прослоек. Не очень удобно. Поэтому и придумали другие способы, о которых расскажу в следующих постах.
Find the way out. Stay cool.
#cppcore #cpp20 #cpp23
#опытным
Начнем рассказ о том, как люди до С++23(то есть до сих пор) жили с оператором[], принимающим только один параметр.
И начнем мы с банальщины. Вот у нас есть класс матрицы. По всем канонам С++ мы должны получать доступ к ее элементам вот так matrix[i][j]. Этот формат сохраняет констистентность с доступом к элементам одномерных массивов.
Однако сразу натыкаемся на проблему. Класс один, а вызываем мы оператор два раза. Несостыковочка.
Ее решает паттерн прокси. Мы создаем прокси класс и возвращаем его объект из первого индекса. Дальше у этого прокси класса определяем оператор[] и на выходе получаем наш элемент.
Условно, из первого оператора возвращаем ссылку на строку матрицы, а из второго - уже сам элемент.
Выглядит это примерно так:
template <typename T>
struct ArraySpan {
ArraySpan(T * arr, size_t arr_size) : data_{arr}, size_{arr_size} {}
ArraySpan(T * arr_begin, T * arr_end)
: data_{arr_begin}
, size_{std::distance(arr_begin, arr_end)} {}
T& operator[](std::size_t i) {
return *(data_ + i);
}
size_t size() const {return size_;}
T * data() {return data_;}
private:
T * data_;
size_t size_;
};
template <typename T>
struct Matrix {
Matrix() = default;
Matrix(size_t rows, size_t cols, T init) : ROWS{rows}, COLS{cols}, data(ROWS * COLS, init) {}
Matrix(Matrix const&) = default;
ArraySpan<T> operator[](std::size_t row) {
return ArraySpan{data.data() + row * COLS, COLS};
}
std::vector<T>& underlying_array() { return data; }
size_t row_count() const { return ROWS;}
size_t col_count() const { return COLS;}
private:
size_t ROWS;
size_t COLS;
std::vector<T> data;
};
int main() {
Matrix mtrx(4, 5, 0);
auto& interval_buffer = mtrx.underlying_array();
std::iota(interval_buffer.begin(), interval_buffer.end(), 0);
for (int i = 0; i < mtrx.row_count(); ++i) {
for (int j = 0; j < mtrx.col_count(); ++j) {
std::cout << std::setw(2) << mtrx[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
for (int i = 0; i < mtrx.row_count(); ++i) {
auto row = mtrx[i];
for (int j = 0; j < row.size(); ++j) {
std::cout << std::setw(2) << row[j] << " ";
}
std::cout << std::endl;
}
}
В примере мы 2 раза проходимся по матрице, чтобы продемонстрировать возможность индексации элементов через объект самой матрицы и объекта строки.
Можно было бы конечно не писать отдельно наш прокси тип ArraySpan, а использовать готовый std::span из С++20, но оставим так. Идея использовать такой легковесный объект понятна - нам не нужно лишнего оверхеда на копирование или создание сложного объекта, чтобы просто получить доступ к элементу матрицы.
Ну и здесь мы можем использовать прокси тип в качестве реальной строки матрицы, и не скрывать его в кишках класса. Так мы получаем доступ к большей вариативности в оперировании матрицами. Например, можно делать скалярное произведение строк и прочее.
В чем недостаток такого способа? Куча прокси классов, которые возможно и не нужны. Возможно, нам просто нужна двумерная структура, чтобы получать доступ к конкретным элементам. Использование карты для морского боя не предполагает использование отдельных строк или столбцов. Хотелось бы просто индексировать конкретные элементы. Но тем не менее мы вынуждены использовать прокси класс.
А если структура трухмерная? Уже 2 прокси нужно будет. Больше вложенность - больше классов-прослоек. Не очень удобно. Поэтому и придумали другие способы, о которых расскажу в следующих постах.
Find the way out. Stay cool.
#cppcore #cpp20 #cpp23
Еще один плюс RAII
#опытным
Основная мотивации использования raii - вам не нужно думать об освобождении ресурсов. Мол ручное управление ресурсами небезопасно, так как можно забыть освободить их и вообще не всегда понятно, когда это нужно делать.
Но не все всегда зависит от вашего понимания программы. Вы можете в правильных местах расставить все нужные освобождения, но код будет все равно небезопасен. В чем проблема? В исключениях.
Это такие противные малявки, которые прерывают нормальное выполнение программы в исключительных ситуациях. Так вот вы рассчитываете на "нормальное выполнение программы" и, исходя из этого, расставляете освобождения. А тут бац! И программа просто не доходит до нужной строчки.
Простой код запроса к базе с кэшом. Что будет в том случае, если метод Select бросит исключение? unlock никогда не вызовется и мьютекс коннектора к базе будет навсегда залочен! Это очень печально, потому что ни один поток больше не сможет получить доступ к критической секции. Даже может произойти deadlock текущего потока, если он еще раз попытается захватить этот мьютекс. А это очень вероятно, потому что на запросы к базе скорее всего есть ретраи.
Мы могли бы сделать обработку исключений и руками разлочить замок:
Однако самое обидное, что исключения, связанные с работой с базой, мы даже обработать не может внутри метода SelectWithCache. Это просто не его компетенция и код сверху некорректен с этой точки зрения.
А снаружи метода объекта мы уже не сможем разблокировать мьютекс при обработке исключения, потому что это приватное поле.
Выход один - использовать RAII.
При захвате исключения происходит раскрутка стека и вызов деструкторов локальных объектов. Это значит, что в любом случае вызовется деструктор lg и мьютекс освободится.
Спасибо Михаилу за идею)
Stay safe. Stay cool.
#cpp11 #concurrency #cppcore #goodpractice
#опытным
Основная мотивации использования raii - вам не нужно думать об освобождении ресурсов. Мол ручное управление ресурсами небезопасно, так как можно забыть освободить их и вообще не всегда понятно, когда это нужно делать.
Но не все всегда зависит от вашего понимания программы. Вы можете в правильных местах расставить все нужные освобождения, но код будет все равно небезопасен. В чем проблема? В исключениях.
Это такие противные малявки, которые прерывают нормальное выполнение программы в исключительных ситуациях. Так вот вы рассчитываете на "нормальное выполнение программы" и, исходя из этого, расставляете освобождения. А тут бац! И программа просто не доходит до нужной строчки.
std::shared_ptr<Value> DB::SelectWithCache(const Key& key) {
mtx_.lock();
if (auto it = cache.find(key); it != cache_.end()) {
mtx_.unlock();
return it->second;
} else {
std::shared_ptr<Value> result = Select(key);
cache.insert({key, result});
mtx_.unlock();
return result;
}
}
Простой код запроса к базе с кэшом. Что будет в том случае, если метод Select бросит исключение? unlock никогда не вызовется и мьютекс коннектора к базе будет навсегда залочен! Это очень печально, потому что ни один поток больше не сможет получить доступ к критической секции. Даже может произойти deadlock текущего потока, если он еще раз попытается захватить этот мьютекс. А это очень вероятно, потому что на запросы к базе скорее всего есть ретраи.
Мы могли бы сделать обработку исключений и руками разлочить замок:
std::shared_ptr<Value> DB::SelectWithCache(const Key& key) {
try {
mtx_.lock();
if (auto it = cache.find(key); it != cache_.end()) {
mtx_.unlock();
return it->second;
} else {
std::shared_ptr<Value> result = Select(key);
cache.insert({key, result});
mtx_.unlock();
return result;
}
}
catch (...) {
Log("Caught an exception");
mtx_.unlock();
}
}
Однако самое обидное, что исключения, связанные с работой с базой, мы даже обработать не может внутри метода SelectWithCache. Это просто не его компетенция и код сверху некорректен с этой точки зрения.
А снаружи метода объекта мы уже не сможем разблокировать мьютекс при обработке исключения, потому что это приватное поле.
Выход один - использовать RAII.
std::shared_ptr<Value> DB::SelectWithCache(const Key& key) {
std::lock_guard lg{mtx_};
if (auto it = cache.find(key); it != cache_.end()) {
return it->second;
} else {
std::shared_ptr<Value> result = Select(key);
cache.insert({key, result});
return result;
}
}
При захвате исключения происходит раскрутка стека и вызов деструкторов локальных объектов. Это значит, что в любом случае вызовется деструктор lg и мьютекс освободится.
Спасибо Михаилу за идею)
Stay safe. Stay cool.
#cpp11 #concurrency #cppcore #goodpractice
Не вызывайте пользовательский код по локом
#опытным
Вы пишите свой потокобезопасный класс, вставляете свои любимые примитивы синхронизации, обмазываете это все подходящим интерфейсом. И тут вам понадобилось вызвать какой-то пользовательский код, какой-нибудь метод или функцию, которую вы сами не писали. И вы это вставляете под замок. Ошибка!
А вдруг эта функция очень долго выполняется? Какой-нибудь долгий запрос к базе. Тогда вы будете долго держать замок, что не очень хорошо. Другие потоки будут потенциально простаивать в ожидании вашего мьютекса. Это может сильно пессимизировать скорость обработки данных. Код под защитой должен быть максимально коротким.
А если функция сама использует блокировку? Локать несколько мьютексов подряд - плохая затея. Это повышает вероятность возникновения дедлока и придется заботиться о его предотвращении. Что довольно сложно сделать с кодом, который вы неполностью контролируете.
А если функция кинет исключение, а вы блокируете мьютекс без оберток(осуждаем)? Мьютекс не освободится. И при повторной его блокировке сразу получили UB. Это как раз пример из предыдущего поста.
То есть делайте все приготовления за пределами критической секции. Достаньте из базы нужные данные, решите большую систему уравнений, посчитайте факториал чего-нибудь на листочке, подумайте о великом, покурите(не пропаганда, администрация канала решительно осуждает никотин) и только потом локайте свою структуру данных, быстро поменяйте там что-нибудь и выходите. Вот примерно так оно должно быть.
GetDataWithCache - это метод интерфейсного класса IStorage и мы сделали приватный виртуальный метод GetData. За то, что делает GetData мы не отвечаем и там потенциально может быть много реализаций. При получении данных из файла там может быть еще один лок. А общение с базой может занять довольно много времени. Поэтому мы просто отпускаем замок перед вызовом этого метода и не паримся о последствиях.
Конечно, бывают всякие ситуации. Но, если у вас есть большая критическая секция, то это как минимум признак того, что неплохо бы посмотреть новым взглядом на это безобразие и, возможно, получится что-то улучшить.
Кстати, в этом примере есть одна не совсем очевидная проблема(помимо более очевидных хехе). Она не относится непосредственно к теме поста. Можете попробовать в комментах ее найти и предложить решение.
#goodpractice
#опытным
Вы пишите свой потокобезопасный класс, вставляете свои любимые примитивы синхронизации, обмазываете это все подходящим интерфейсом. И тут вам понадобилось вызвать какой-то пользовательский код, какой-нибудь метод или функцию, которую вы сами не писали. И вы это вставляете под замок. Ошибка!
А вдруг эта функция очень долго выполняется? Какой-нибудь долгий запрос к базе. Тогда вы будете долго держать замок, что не очень хорошо. Другие потоки будут потенциально простаивать в ожидании вашего мьютекса. Это может сильно пессимизировать скорость обработки данных. Код под защитой должен быть максимально коротким.
А если функция сама использует блокировку? Локать несколько мьютексов подряд - плохая затея. Это повышает вероятность возникновения дедлока и придется заботиться о его предотвращении. Что довольно сложно сделать с кодом, который вы неполностью контролируете.
А если функция кинет исключение, а вы блокируете мьютекс без оберток(осуждаем)? Мьютекс не освободится. И при повторной его блокировке сразу получили UB. Это как раз пример из предыдущего поста.
То есть делайте все приготовления за пределами критической секции. Достаньте из базы нужные данные, решите большую систему уравнений, посчитайте факториал чего-нибудь на листочке, подумайте о великом, покурите(не пропаганда, администрация канала решительно осуждает никотин) и только потом локайте свою структуру данных, быстро поменяйте там что-нибудь и выходите. Вот примерно так оно должно быть.
std::shared_ptr<Value> IStorage::GetDataWithCache(const Key& key) {
std::unique_lock ul{mtx_};
if (auto it = cache.find(key); it != cache.end()) {
return it->second;
} else {
ul.unlock(); // HERE
std::shared_ptr<Value> result = GetData(key);
ul.lock();
cache.insert({key, result});
return result;
}
}
GetDataWithCache - это метод интерфейсного класса IStorage и мы сделали приватный виртуальный метод GetData. За то, что делает GetData мы не отвечаем и там потенциально может быть много реализаций. При получении данных из файла там может быть еще один лок. А общение с базой может занять довольно много времени. Поэтому мы просто отпускаем замок перед вызовом этого метода и не паримся о последствиях.
Конечно, бывают всякие ситуации. Но, если у вас есть большая критическая секция, то это как минимум признак того, что неплохо бы посмотреть новым взглядом на это безобразие и, возможно, получится что-то улучшить.
Кстати, в этом примере есть одна не совсем очевидная проблема(помимо более очевидных хехе). Она не относится непосредственно к теме поста. Можете попробовать в комментах ее найти и предложить решение.
#goodpractice