C++ Embedded
Ir al canal en Telegram
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Mostrar más446
Suscriptores
+124 horas
+27 días
+430 días
Archivo de publicaciones
446
non-deduced lambda
В одной маленькой компании на ежегодной традиционной пресс-конференции большого начальства каждый раз некий аноним задавал один и тот же вопрос: "Почему меня бьет тимлид?"
Ответа на этот вопрос я не помню, поскольку в этот момент от тимлида мне прилетел джеб, кросс, левый хук и правый лоу-кик.
Это у него просто софт скиллов не хватало, а кунг-фу скиллы так и перли из ушей. Я к тому, что в разработке обязательно должно быть пространство для дискуссии, не оканчивающейся судебным поединком.
Вот в предложении P0428R0 с этим все было в порядке. Горячо обсуждался вопрос, как правильно совмещать
auto-аргументы и явный шаблонный список параметров.
Нужно ли позволять такое соседство? А если непотребство разрешать, то как это должно выглядеть?
Куда ставить ставить неявный список шаблонов? Перед явным списком или после?
[]<typename T>(T, auto) // => template <typename T, typename Invented>
[]<typename T>(auto, T) // => template <typename Invented, typename T>
Как проходила эта дискуссия, я не знаю, наверняка так же, как и все заседания комитета в моем представлении: повышенные тона, брань, переходящая в поножовщину, полицейские водометы, обезьянник... всегда что-то такое себе представляю, когда читаю их протоколы. Согласитесь, это читается между строк:
В итоге мы решили дозволить совмещать оба синтаксиса (и auto, и явный список), будем просто добавлять неявный шаблонный список в конец явного, потому что это самое простое решение, которое не ограничивает выразительность и не нарушает консистентность того, что уже было сделано в предложении о концептах. Если же окажется, что мы встали на скользкий путь, то мы живо все переиграем и сделаем вид, что изначально запрещали использовать двойной синтаксис.Судя по поведению GCC сейчас, товарищи пошли верной дорогой. Ничего необычного в поведении таких лямбд нет. Шаблон, созданный
auto аргументами, просто добавляется в конец явного шаблонного списка параметров. Никаких неожиданностей.
Например, была у нас такая лямбда
auto p = []<class ... T>(T ... t) {};
Вызывать ее можно абсолютно по-разному, но пакет шаблонных параметров все равно будет довыведен из аргументов функции.
p.operator()<int, double, int, int>(1, 1.0, 1, 1);
Все согласно стандарту: параграф 13.10.3.2 Deducing template arguments from a function call [temp.deduct.call]:
Вывод типов пакета шаблонных параметров выполняется из соответствующей группы аргументов функции, если эта группа стоит последней в списке аргументов.То есть, если даже оператору
p.operator() намеренно сообщить короткий список типов <int, double>, он сопоставит первые типы с T, а дальше пойдет выводить из переданных в функцию аргументов.
Если мы добавим любой аргумент после пакета, то это разрушит магию выведения типов и ввергнет пакет в так называемый non-deduced context.
Согласно 13.10.3.6 Deducing template arguments from a type [temp.deduct.type], существует много сложных для вывода мест, когда шаблонные параметры приходится указывать явно. Есть даже метафункция std::identity_t для искусственного создания такого контекста.
Так вот, функцию такого вида
auto p = []<class ... T>(T ... t, int x) {};
можно вызвать, только указав явно все шаблонные параметры:
p(1, 1, 2, 3); // Error
p.operator()<int, int, int>(1, 1, 2, 3); // Ok
Компилятор справедливо полагает, что если нет указанных явно параметров, значит, пакет T должен быть пустым.
Те же соображения применимы к следующей функции:
auto p = []<class ... T>(auto ... x, T ... t) {};
Пакет auto-параметров идет не последним, в невыводимом контексте. Пакет T, наоборот, в выводимом. Поэтому не имеет значения, что мы напишем
p(1, 1, 1, 1);
p.operator()<int, int>(1, 1.0, 1, 1);
Пакет T заграбастает все типы аргументов! В пакете x останется только большое жирное нифига, но мы об этом сразу не узнаем.
Имеет смысл переставить местами пакеты аргументов (T ... t, auto ... x), чтоб невыводимый тип шел в списке шаблонных параметров первым.
Тогда мы смогли бы его задать явно.446
lambda templates
"Треть марта уже того, а колотун такой, что бубенцы звенят..." - громко возмущался в беспроводную гарнитуру гражданин, осторожно ступая по отполированному тысячами копчиков льду.
Как хорошо я понимаю тебя, случайный прохожий! Весну мы получили, но в ней нет тепла. Обман! Все равно что купить шаурму на Средном рынке, а в ней нет кишечной палочки. Это как стандарт c++14 обзаведшийся де-факто шаблонными лямбдами, вся шаблонность коих держится на
auto-аргументах. Список параметров шаблона для лямбд не завезли. Хотя такое соображение имелось в изначальном предложении N3418. Только принято было не оно, а переработанное предложение-наследник N3559, которое сосредоточилось исключительно на auto-центричном синтаксисе и немножечко предало идеалы метапрограммирования.
[](auto x) { ... }
Такая запись вроде бы удобна, понятна, лаконична, в конце концов. Однако в некоторых моментах эта хваленая лапидарность играет против нас.
Примерно такие соображения и были изложены Louis Dionne в сентябре 2016 года в предложении Familiar template syntax for generic lambdas.
Все хорошо, мол, нам все нравится, но почему бы не сделать список шаблонных параметров? А то гибкости не хватает.
Например, хорошо бы записать аргумент через std::vector, чтоб сразу было понятно, кого мы ждем на входе функции. С auto так не получится, а вот со списком шаблонных параметров - пожалуйста:
auto f = []<typename T>(std::vector<T> vector) { ... };
Очень больно работать с типами auto-аргументов внутри
auto f = [](auto const& x) { using T = std::decay_t<decltype(x)>; ... }
или даже не внутри, а во втором аргументе
auto advance = [](auto& it, typename std::decay_t<decltype(it)>::difference_type n) { ... };
Гораздо приятнее было бы разместить тип снаружи:
auto f = []<typename T>(T const& x) {
T copy = x;
T::static_function();
using Iterator = typename T::iterator;
};
auto advance = []<typename It>(It& it, typename It::difference_type n) { ... };
Проще будет ожидать на входе массив:
[]<typename T, int N>(T (&a)[N]) { ... }
Ну и perfect forwarding выглядит просто ужасно:
auto f = [](auto&& ...args) { return foo(std::forward<decltype(args)>(args)...); };
Cам Scott Meyers написал об этом грустный пост.
Гораздо лучше, если бы все было как при бабушке:
auto f = []<typename ...T>(T&& ...args) { return foo(std::forward<T>(args)...); };
Комитету ничего не оставалось, как под давлением этих железобетонных аргументов как можно скорее принять явный шаблонный список для лямбд в стандарт. Не в c++17, конечно, не в сказке живете, а в с++20.
"Позвольте!" - скажут некоторые, - "Мы уже использовали это и очень даже давно, еще при Медведеве!".
auto x = []<class T>() -> void {};
Работало же для GCC даже при с++14. Да, работало, но подпольно!
Если сейчас внимательно смотреть на предупреждения, а не отключать их, то обязательно увидим:
warning: lambda templates are only available with '-std=c++20' or '-std=gnu++20' [-Wc++20-extensions]Все потому, что GCC, а по слухам и clang, бежали впереди паровоза и реализовали эту фичу в качестве расширения языка. Собственно, в тексте предложения можно найти тому подтверждение в пункте
5 Implementation experience.
Расширение для обобщенной лямбды было реализовано GCC аж в 2009 году в качестве эксперимента. Шалость удалась.
Тем не менее, до с++20 использовать это незаконно, только на свой страх и риск. Департамент цензуры не одобряет.446
epstein lambdas
Лямбда. Ее появления мы ждали больше, чем публикации файлов Эпштейна. В минувшем феврале разработчики праздновали двадцатилетие первого и второго предложения включить лямбда-выражения в стандарт. Хоть идея витала в воздухе с начала двухтысячных, первым предложением стал документ N1958 с незатейливым названием
A proposal to add lambda functions to the C++ standard за авторством некого Valentin Samko. Опубликован точно 23-го февраля. Видимо, Валя не знал, чем занять себя в выходной.
Иронично, но спустя три дня, 26-го февраля, очнулся и сам Bjarne Stroustrup, а может, и не сам, возможно, был разбужен товарищами Jeremiah Willcock, Jaakko Jarvi, Doug Gregor, Andrew Lumsdaine. Почувствовав жжение в районе поясницы и не желая терять гордое звание новаторов и реформаторов, могучая кучка срочно опубликовала свою работу под названием Lambda expressions and closures for C++. Хотя мне лично приятно считать это босяцким подгоном от Страуструпа.
Как бы то ни было, спустя несколько лет мы все с радостью обнаружили лямбды в новом стандарте c++11.
Пока еще совсем простые и незатейливые:
lambda-expression: lambda-introducer lambda-declarator(opt) compound-statementВ
lambda-introducer происходит захват значений, compound-statement - тело лямбды в фигурных скобках.
За lambda-declarator скрывается необязательный список аргументов в круглых скобках, после которого могло идти ключевое слово mutable.
Самая простая лямбда:
auto x = [](){};
Если убрать сахар, то выражение раскроется в довольно тривиальный класс:
class __lambda_18_10 {
public:
inline /*constexpr */ void operator()() const {}
};
По умолчанию operator() имеет спецификатор const, а ключевое слово mutable может избавить нас от этой напасти.
class __lambda_18_10 {
public:
inline /*constexpr */ void operator()() {} // mutable
...
Казалось бы, разрабатывай теперь в свое удовольствие, но эйфория прошла, взамен появились новые запросы.
Уже в 2012 году было опубликовано предложение N3418 Proposal for Generic (Polymorphic) Lambda Expressions, где некие господа Faisal Vali, Herb Sutter и Dave Abrahams прямо заявили, что не согласны с политикой партии.
Стандарт C++11 дает нам нешаблонные лямбда-выражения. Ведомые духом старой школы и руководствуясь соображениями, которые когда-то привели к появлению auto, мы предлагаем добавить в C++ шаблонные лямбда-выражения.В самом деле, использовать лямбду в обобщенном матапрограммировании было не слишком удобно, местами приходилось изгаляться, как зайцу в алюминиевых штанах.
std::for_each( begin(v), end(v), [](decltype(*begin(v)) x){ std::cout << x; });
Писать каждый раз decltype(*begin(v)) нам вообще не улыбается, хуже только каждый раз это читать и вспоминать, что это такое и зачем.
В следующем году предложение трансформировалось в N3559, где уже окончательно оформился синтаксис.
Теперь параметр лямбды может быть типа auto:
auto p = [](auto f) {};
Что по задумке авторов трансформируется в такой код:
class __lambda_29_12 {
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0 f) const {}
...
Хорошо, раз лямбду можно рассматривать как обычный класс, а метод с auto как обычный шаблонный, то почему бы не использовать это на полную?
Вот возьмем и определим такую лямбду:
auto p = [](auto x) { return sizeof(x); };
Функцию эту предполагается использовать таким образом:
p(1); // вернет 4Здесь мы полагаемся на автоматический вывод типа. Однако можно явно указать, какой тип должен быть использован!
p.operator()<uint64_t>(1); // вернет 8
Громоздко, но никакой приятной сокращенной записи для такого случая не завезли. Странное не должно быть легкодоступно.
И на этом странное не заканчивается.446
Больше двадцати лет работы с самым хитровыделанным превосходным языком, и все, что я получил - этот маленький значок:)
Нет, я всем доволен! Ведь могли бы и шашкой рубануть...
446
tls. __gthread_once
Итак, зима и не думает сдавать позиции, навалила нам снега за шиворот, будьте-нате! Ну и пожалуйста, ну и ладно, я тогда вообще перестану на улицу выходить. Что я, дурнее дворников, что ли?! Зато будет много свободного времени, чтоб докончить сагу о
std::call_once.
Так вот, TLS дал нам возможность хранить целевую функцию только в "потоковой" памяти, что упрощает реализацию __once_proxy, но потребует умственного усилия для согласования одновременных вызовов. Вариантов, где такую синхронизацию можно осуществить, не так уж и много. Как говорил товарищ Сталин: "Нам стоит присмотреться к функции __gthread_once". Ладно, может, и не говорил, но намек все поняли правильно.
Я более чем уверен, что эту задачу уже успешно решили не только лишь все, поэтому заглянем в поисках best practice в код glibc.
Определение __gthread_once_t для нашего случая приводит нас к забавной структуре:
struct __pthread_once {
int __run;
__pthread_spinlock_t __lock;
};
Внезапно тип __gthread_once_t это не просто целочисленная переменная, но еще и некое средство синхронизации - __pthread_spinlock_t. Спинлок собственной персоной.
Нам тоже нужно что-то подобное!
Первой мыслью было определить тип __gthread_once_t как std::atomic<uint8_t>, но об этом сразу же пришлось забыть. Файл gthr-user.h, где определяется __gthread_once_t, сам встраивается окольными путями в <atomic>, ничего хорошего от зацикливания этой зависимости не будет.
В принципе, можно попробовать обойтись volatile int, типы до 32 бит на наших stm все равно, что atomic. Однако GCC может предложить нам кое-что особенное.
Пусть __gthread_once_t остается целочисленным типом:
using __gthread_once_t = uint8_t;Зато
__gthread_once будет реализовано хитрее:
inline int __gthread_once(__gthread_once_t *__once, void (*__func) (void)) {
int result {};
uint8_t old_value {*__once};
if (old_value == __gthread_once_t{}) {
if (__atomic_compare_exchange_n(__once, &old_value, 1, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {
__func();
}
}
return result;
}
Если целевая функция не равна нулю, то быстро выходим из функции, работа уже сделана.
Иначе пытаемся атомарно заменить значение флага __once с нуля на единицу функцией __atomic_compare_exchange_n.
Это встроенная GCC функция для работы с атомиками, реализует атомарную операцию Compare-and-Swap.
bool __atomic_compare_exchange_n(type *ptr, type *expected, type desired, bool weak, int success_memorder, int failure_memorder)
где ptr - это указатель на атомарную переменную;
expected - указатель на переменную, содержащую ожидаемое значение;
desired - значение, которое должно быть записано;
weak: тип операции (false - это гарантированный успех, если значения равны [strong], true - может дать ложный отказ, даже при равенстве значений [weak])
success_memorder - порядок памяти при успешной записи (__ATOMIC_SEQ_CST по умолчанию).
failure_memorder - порядок памяти при неудачном сравнении.
Концепция CAS, конечно, невероятно проста, но я иногда забываю, что куда копируется.
На самом деле, сравниваем содержимое *ptr и *expected, если они совпадают, то быстро, пока никто не помешал, записываем desired в *ptr и возвращаем true.
Если значения не совпадают, то в *expected отправляется текущее содержимое *ptr, а возвращается false, конечно.
В нашем случае мы будем ожидать, что значение *__once нулевое, и если наши надежды оправдались, то записываем туда единицу и уходим на выполнение __func().
Все потоки-преследователи, даже если они прошли первое ветвление с проверкой old_value, то __atomic_compare_exchange_n вернет false, т.к. первый вызов атомарно поменял значение __once. Дело сделано, можно отдыхать, пить пиво, ругать сантехников, ждать закрытия Телеги.446
Очень давно, когда эксперименты со стандартными потоками во FreeRTOS только начались, мы создали thread local storage. Они же переменные с общим именем, но живущие в каждом потоке на своем участке памяти.
Плохо, что ли? Хорошо, не зря же наколхозили, попробуем это применить здесь.
Да уж, сам механизм-то мы добавили, но не известили об этом компилятор. Поэтому он и не подумал собрать обрезки плюсовой стандартной библиотеки с TLS.
Скорее же исправим ситуацию и определим макрос
_GLIBCXX_HAVE_TLS в настойках проекта.
Теперь стандартная библиотека переключается на использование потокоспецифичного описания функтора, который будет исполнен в вызове std::call_once.
Описание выглядит следующим образом:
__thread void* __once_callable; __thread void (*__once_call)();Всего лишь пара переменных, в которых хранится сакральные знания о целевой функции. Обязательно нужно их правильно заполнить перед решающим вызовом
__once_proxy. Поэтому придется полностью переделать нашего RAII помощника внутри once_flag.
struct _Prepare_execution {
template<typename _Callable>
explicit _Prepare_execution(_Callable& __c) {
__once_callable = std::__addressof(__c);
__once_call = [] { (*static_cast<_Callable*>(__once_callable))(); };
}
~_Prepare_execution() {
__once_callable = nullptr;
__once_call = nullptr;
}
...
};
Вот потому понадобилось аж две переменные. В __once_callable мы заносим адрес реальной целевой функции, которую предстоит выполнить. Только вызвать указатель на void в качестве функции - идея так себе. Придется в __once_call записать еще и лямбду, где выполняется сохраненная функция. Там она приводится обратно к типу _Callable и исполняется.
В деструкторе мы просто подчищаем за собой, обнуляя все переменные.
Еще применение TLS приведет к опрощению функции __once_proxy. Как вы уже догадались, там не останется никаких игр c мьютексами, только прямой вызов __once_call:
extern "C" void __once_proxy() {
__once_call();
}
Вроде бы все пока логично, мы избавились от глобального мьютекса, запихнули целевую функцию в уникальное для каждого потока хранилище. Но как теперь обеспечить единственное выполнение функции при одновременном вызове из нескольких потоков?446
tls. std::call_once
Зимняя сказка продолжается уже в виде абсурдного балагана. К концу сезона снег уже некуда убирать. Загорелые теплолюбивые дворники, видимо, впали в коллективное уныние и апатично ждут всеобщего таяния. Осталось месяц перетерпеть, и заструится. А может, и не заструится.
Пока же на улице пахнет паленым сцеплением, и то тут, то там раздается рев буксующих в мерзкой снежной каше машин.
Лет десять назад зима тоже была сугробистая, да настолько, что лопату я возил прямо в салоне автомобиля. Однажды вечером, когда мы с другом мирно сидели дома и приобщались к высокому искусству, нам позвонила хорошенькая знакомая и поведала душераздирающую историю. Бедолага крепко засела на какой-то нечищеной дороге и уповала теперь только на грубую физическую силу. Явно на что-то намекала. Знакомая была хоть немного странная, но недурна собой, поэтому мы тут же сорвались и через пять минут были на месте жуткого происшествия.
Как можно было застрять на Шевроле-Ниве в малюсеньком сугробе?
- Эээ... да ты просто в раскачку пусти и вылезешь, - недоуменно сказал мой товарищ, заглянув под днище.
На симпатичном лице нашей хорошей знакомой отразилось отчаяние японского туриста, которого впервые попросили передать за проезд в нашей маршрутке.
- Понятно, - говорю, - давай лучше я сяду за руль...
- Нет-нет-нет, вы же в страховку не вписаны, и вообще, просто подтолкните!
Чертыхаясь, мы полчаса раскачивали внедорожник-полукровку, борясь не столько со снегом, сколько с неуместными и порой неожиданными нажатиями на газ. Наконец, автомобиль выскочил из снежного плена.
- Спасибо! - успела крикнуть барышня из окна, прежде чем неосторожно вдавить тапку в пол.
Машина резко дернулась и с заносом ударилась в бордюр боком. Кажется, подломилась стойка, потому как колесо теперь под невообразимым углом торчало из арки.
Над городом раздался крик, бешеный, страстный и дикий, крик простреленной навылет волчицы.
- Бежим, - друг дернул меня за рукав, - она же нас до сервиса заставит тачку толкать.
- Знаешь, эта твоя знакомая мне никогда не нравилась... а мы еще "Сумерки" не досмотрели.
Мы синхронно выключили телефоны и спешно ретировались.
А избавить от муторной возни с глобальными мьютексами для решения проблемы однократного вызова
сall_once могут три волшебных буквы: TLS.446
__gthread_once std::call_once
- Телегу-то вашу запретили, Бронислав Арнольдович! - радостно доложила мне соседка, едва лифт начал набирать высоту.
- Откуда информация, мадам?
Имени соседки я не помнил, но и я не был ни Брониславом, ни уж тем более Арнольдовичем.
- Мадемуазель! - игриво пригрозила пальчиком девица в летах, - да все в телеграме обсуждают!
- Анонимные каналы врать не будут, - бросил я, просачиваясь в едва приоткрывшиеся двери.
- Заходите за солью!
Да знаем мы твою соль... противная мелкая йодированная, а хочется душевной костромской черной. Отмахнувшись от прилетевшей в спину фразы, я побежал домой. Надо успеть поделиться с миром последними результатами бесчеловечных опытов по колхозизации
call_once во FreeRTOS. Рывком раскрыв ноут, я начал печатать...
Вызов целевой функции происходит в заключительной части call_once:
if (int __e = __gthread_once(&__once._M_once, &__once_proxy))
__throw_system_error(__e);
Здесь мы зовем функцию __gthread_once, передавая ей указатель на _M_once - состояние флага и еще указатель на __once_proxy.
Это не что иное, как глобальная функция для работы с глобальными объектами. Вот там и живет вызов целевой функции.
Допишем сначала реализацию __gthread_once в файле gthr-user.h
inline int __gthread_once (__gthread_once_t *__once, void (*__func) (void)) {
int result {};
if (*__once == __gthread_once_t{}) {
try {
__func();
++(*__once);
} catch(...) {
result = EINVAL;
}
}
return result;
}
Здесь мы проверяем состояние флага __once, вдруг функция уже исполнена. Если нет, то пытаемся это сделать в блоке try.
После успешного завершения функции __func, мы меняем состояние флага. Если же вылетим по исключению, то поменять значение __once мы уже не успеем.
Перехватив исключение, мы вернем признак ошибки, например, EINVAL. Тогда какой-нибудь другой поток может попытаться выполнить эту функцию.
Впрочем, у меня исключения отключены, поэтому можно упростить:
if (*__once == __gthread_once_t{}) {
__func();
++(*__once);
}
return result;
Будем изо всех сил надеяться, что целевая функция не сломает нам прошивку.
Теперь подходим к самому главному, к функции __once_proxy. Что скрывает она за эпатажным названием?
Ее определение зависит от окружения, в нашем случае подойдет такое:
extern "C" void __once_proxy() {
std::function<void()> callable = std::move(__once_functor);
if (std::unique_lock<mutex>* lock = set_lock_ptr(nullptr)) {
lock->unlock();
}
callable();
}
Во-первых, функция разблокирует глобальный мьютекс. Во-вторых, непосредственно вызывает глобальный функтор, который мы ранее записали.
Явная разблокировка глобального мьютекса точно не для вылета по исключению. Нет, в этом случае деструктор RAII помощника все равно вызовется, их для этого и делают. Просто представьте, что целевая функция выполняется очень-очень долго, а call_once-инициализаций много. Поэтому важно до выполнения длинной функции освободить мьютекс, поскольку он один на всех. Еще это предотвращает deadlock, когда из одного call_once нужно вызвать другой.
Таким образом, вызов запрошенной функции через call_once произойдет только один раз, что нам и требовалось.
Хотя возня с глобальными объектами не очень воодушевляет, у нас же было реализовано что-то подходящее для этого случая?446
multithread std::call_once
Время вернуться в наш родной кибер-колхоз с потоками и FreeRTOS-ом. Из-за этих самых потоков мы не стали реализовывать однопоточный
once_flag, несмотря на его заманчивую простоту. У нас уже определен _GLIBCXX_HAS_GTHREADS, а это значит, что собираться будет иная реализация:
struct once_flag {
constexpr once_flag() noexcept = default;
private:
__gthread_once_t _M_once = __GTHREAD_ONCE_INIT;
struct _Prepare_execution;
template<typename _Callable, typename... _Args>
friend void call_once(once_flag& __once, _Callable&& __f, _Args&&... __args);
};
Кажется, эта имплементация даже проще, попробуем подстроиться под нее и дописать, чего не хватает.
Внутри структуры у нас есть некий объект _M_once типа __gthread_once_t. Назначение этой переменной несложно угадать - статус исполнения целевой функции. Не будем тут мудрствовать, допишем в файле gthr-user.h
using __gthread_once_t = int32_t;Пусть статус будет целочисленный, где
0 - функция не запускалась, 1 - функция успешно завершилась.
Не забудем определить начальное значение для статуса _M_once.
#define __GTHREAD_ONCE_INIT 0
У флага есть дружественная функция call_once, только ей доверено ковыряться во внутренностях флага.
Функция шаблонная, поэтому общая реализация в заголовочном файле уже есть.
template<typename _Callable, typename... _Args>
void call_once(once_flag& __once, _Callable&& __f, _Args&&... __args) {
auto __callable = [&] {
std::__invoke(std::forward<_Callable>(__f),
std::forward<_Args>(__args)...);
};
once_flag::_Prepare_execution __exec(__callable);
if (int __e = __gthread_once(&__once._M_once, &__once_proxy))
__throw_system_error(__e);
}
Вызов целевой функции мы оборачиваем в __callable объект. Эту лямбду теперь можно вызвать без аргументов.
Далее создается классический RAII объект once_flag::_Prepare_execution, в качестве аргумента ему передается функтор __callable.
Этот помощник должен подготовить окружение к вызову переданного функтора и не дать конкурирующим потокам запустить такой же call_once от себя.
struct once_flag::_Prepare_execution {
template<typename _Callable>
explicit _Prepare_execution(_Callable& __c) {
__once_functor = __c;
__set_once_functor_lock_ptr(&_M_functor_lock);
}
~_Prepare_execution() {
if (_M_functor_lock)
__set_once_functor_lock_ptr(nullptr);
}
private:
unique_lock<mutex> _M_functor_lock{__get_once_mutex()};
_Prepare_execution(const _Prepare_execution&) = delete;
_Prepare_execution& operator=(const _Prepare_execution&) = delete;
};
При создании _Prepare_execution объект заносит исполняемый функтор в глобальную переменную __once_functor типа std::function.
Вторая функция __set_once_functor_lock_ptr устанавливает местный _M_functor_lock как глобальный unique_lock<mutex>, чтоб потом к нему можно было бы получить доступ из глобальной и свободной функции.
Благодаря этому блокировщику только самый быстрый поток сможет завершить создание _Prepare_execution и пойти дальше, а остальные затормозятся на этапе создания _M_functor_lock, поэтому они не смогут испортить нам праздник, перезаписав глобальные переменные.
При уничтожении помощник разблокирует остальных и обнулит указатель на глобальный unique_lock.
Кстати, все _M_functor_lock используют один глобальный мьютекс, который они берут из функции:
mutex& __get_once_mutex() {
static std::mutex once_mutex;
return once_mutex;
}
Итак, все необходимые приготовления сделаны, осталось только исполнить целевую функцию.446
std::source_location
Удушающая волна зноя окатила меня, как только я вышел из здания вокзала в Химедзи. Вторая волна, уже людская, подло ударила в спину и понесла навстречу замку, белой цаплей высившемуся над городской суетой. Еле двигая ногами, я искал хоть какое-то укрытие от яростного солнца, но взгляд натыкался только на бесстыжих обнаженных девиц. Вряд ли оголились они из-за жары, поскольку были из бронзы и, скорее всего, должны были развлечь пресыщенного и скучающего туриста на пути к главной достопримечательности. Впрочем, повышенное внимание к статуям проявлял только один неблагонадежный пенсионер. Он уже минут десять стоял рядом со мной и откровенно пялился на пышные перси. Не мои, наверное.
- Что тебе нужно, старик-извращенец?! - не выдержал я.
- Вы неправильно поняли, - стал оправдываться он, - у меня просто душевная травма! Я бывший работник корпорации "Санко Машинэри". Однажды меня отправили в командировку в Бэйкоку, там мне довелось посетить местное кафе, где белые пили кофе с порционным сахаром. С нашим сахаром! Мы первые придумали упаковывать его в узкие пакетики. Недалекие гайкокудзины отрывали краешек пакетика и высыпали сахар в чашечку!
Мы, значит, придумывали оптимальную форму, предложения писали рационализаторские... презентации презентовали... а они...
Я бегал по заведению и объяснял, что надо ломать стик посредине, так и мусора меньше, и удобнее, а меня просто выставили.
Старик смахнул скупую самурайскую слезу.
- В общем, вылетел я из кафе и тут же совершил сеппуку, или харакири по-вашему. Правда, раньше люди были крепче, и уже вечером я отплясывал на дискотеке.
Тут дед пустился в пляс, а я - бежать. Кому-то из нас точно напекло тыковку.
История эта не просто летний флешбэк в холодный и снежный зимний вечер.
Тут недавно порадовали коллеги кодом логгера, что-то в таком духе:
void log(std::source_location location, std::string_view const msg) {
std::clog << "file: " << location.file_name()
...
}
#define LOGME(MSG) log(std::source_location::current(), MSG)
Обычный подход для логгеров: макрос раскрывается в месте вызова. Есть модный std::source_location - агрегатор сведений об исходном коде. Статический метод current() выдаст имя файла, номер строки и прочие координаты места вызова. Логично, но что-то настораживает.
Все знают, что никто никогда не вернет 2007 год, но попробуем вернуться в 2014 год, когда Robert Douglas опубликовал первую версию предложения N3972 Source-Code Information Capture.
Логгирование, тестирование и иные проверки желают передавать в своих сообщениях сведения об имени файла, номере строки и имени функции. Сейчас эту информацию без дублирования кода можно присовокупить только макросами. Они разворачиваются в месте использования, и интерпретация __FILE__, __LINE__ и __func__ происходит в контексте места вызова. Само по себе это не проблема, но, к несчастью, в результате мы получаем чертову прорву макросов, что пронизывают все уголки кодовой базы.Глядя на эту кодовую плесень, цветущую в каждом проекте, автор поклялся создать такую фичу, что позволила бы функции захватить информацию об исходном коде, вызвавшем ее. Подобную переменную можно применять в проверках:
template<typename T>
void assert_equal(T const& l, T const& r, std::source_location location = std::source_location::current()) {
if (!(l == r)) {
std::clog << "file: " << location.file_name()
...
Либо для логирования:
void log(std::string_view const msg, std::source_location location = std::source_location::current()) {
std::clog << "file: " << location.file_name()
...
Добавляя в качестве аргумента по умолчанию std::source_location::current(), мы предписываем функции current выполняться только при вызове log и захватывать информацию аккурат в месте вызова. Все продумано, чтоб только не возвращаться к мерзким макросам. Поэтому использование функции current в LOGME - это плевок в лицо Роберту! Если бы он это увидел, то уже бежал бы за веревкой.446
single thread std::call_once
Однократный вызов можно реализовать по-разному. Зависит от окружения. Если вдруг вас окружили однопоточные системы, то все элементарно, в чем можно легко убедиться, заглянув в секции для неопределенной
_GLIBCXX_HAS_GTHREADS заголовочного файла mutex:
struct once_flag {
...
enum _Bits : int { _Init = 0, _Active = 1, _Done = 2 };
int _M_once = _Bits::_Init;
bool _M_passive() const noexcept { return _M_once == _Bits::_Done; }
bool _M_activate() { ... }
void _M_finish(bool __returning) noexcept { _M_once = __returning ? _Bits::_Done : _Bits::_Init; }
struct _Active_execution { ... };
}
Понятненько, once_flag может находиться в нескольких состояниях: _Init - функция и не думала исполняться, _Active - исполнение инициировано, _Done - функция выполнена.
Обратите внимание, в составе флага есть RAII помощник:
struct _Active_execution {
explicit _Active_execution(once_flag& __flag) : _M_flag(__flag) { }
~_Active_execution() { _M_flag._M_finish(_M_returning); }
...
once_flag& _M_flag;
bool _M_returning = false;
};
Смысл его существования в том, чтоб при разрушении вызвать функцию _M_finish связанного с ним флага. В качестве аргумента передается значение _M_returning, который по умолчанию false.
Даже не заглядывая в код, на животном уровне понятно, что _M_activate() переводит флаг в состояние _Active, если он был в состоянии _Init до этого. Если же флаг в состоянии _Done, то дело уже сделано и больше ничего не нужно. А вот если флаг в состоянии _Active перед вызовом _M_activate, то что-то пошло не так. Значит, кто-то уже начал исполнять эту задачу, но не закончил по каким-то причинам, чего быть не должно, в результате будет выброшено исключение __throw_system_error(EDEADLK);
Теперь рассмотрим функцию call_once:
template<typename _Callable, typename... _Args>
inline void call_once(once_flag& once, _Callable&& f, _Args&&... args) {
if (once._M_passive()) return;
else if (once._M_activate()) {
once_flag::_Active_execution exec(once);
std::__invoke(std::forward<_Callable>(f), std::forward<_Args>(args)...);
exec._M_returning = true;
}
}
Во-первых, посмотрим, не выполнена ли уже задача, т.е. проверим результат once._M_passive(). Если нет, то пробуем активировать флаг, вызываем _M_activate(). Если не словили исключение, то приступаем к исполнению.
Непосредственно перед запуском создаем RAII объект exec, привязанный к флагу once.
Далее просто вызываем std::__invoke для переданной функции _Callable&& f с аргументами _Args&&... args.
Если вызов функции завершился без исключений и std::abort, то все отлично, у объекта exec вы вручную меняем значение _M_returning на true.
Теперь при разрушении объекта exec, метод _M_finish будет вызван с параметром true, что переведет флаг в состояние _Done и сделает невозможным повторный вызов целевой функции.
Поскольку этот метод применяется в однопоточном окружении, то синхронизировать ничего не нужно.
Это, конечно, база. Очень интересный прием, изящный метод, однако, напомню, мы делаем свой колхозный FreeRTOS с потоками, поэтому нам это не подойдет.446
one-time function calls
Пока я в в 2006 году был занят проблемой демаркации границ между наукой и третьей Готикой, некто Anthony Williams выкатил целый трактат N2139 "Thoughts on a Thread Library for C++".
Хорошо, мол, иметь возможность инициализировать объект абсолютно потокобезопасно, и при этом быть уверенным в однократном исполнении функции инициализации. Это важная и нужная штука для многих библиотек. Не зря же в POSIX имеется функция
pthread_once, а boost запилил функцию call_once.
Это настолько важная фича, что про нее не вспомнили ни в одном предложении допортлендской эпохи, сокрушается автор. Такая халатность, конечно, ужасает до непроизвольного тверка, ведь эта фишка должна быть в каждой плюсовой библиотеке!
Одним из способов добиться желаемого эффекта может быть метод однократного вызова функции.
Автор предлагает вспомнить упомянутый выше call_once и включить его в стандарт. Эта функция исполнит некое действие ровно один раз и клянется Страуструпом, что все потоки, пытающиеся выполнить тот же call_once, точно подождут окончания самого раннего вызова.
Как и прочие подобные функции, call_once будет использовать специальный once_flag флаг, гарантирующий гибкость и однократное выполнение.
Предложенный интерфейс once_flag:
struct once_flag {
constexpr once_flag();
};
Общо, лапидарно. Пока замнем для ясности.
Сигнатура call_once виделась автору такой:
template<typename Callable>
void call_once(once_flag& flag, Callable func);
Параметр func должен был быть копируемым, да чтоб без побочных эффектов при выполнении, и без бросков исключениями.
Заботясь так же об удобстве пользователей, автор предложил func возвращать void и не принимать никаких аргументов.
Однако такая простота не прошла комитет. В итоге сошлись на таком варианте:
template <class Callable, class ...Args>
void call_once(std::once_flag &flag, Callable &&f, Args && ...args);
Аргументы есть, но их даже не придется синхронизировать, ведь предполагается однократный вызов из единственного потока.
Насчет исключений тоже получилось интересно.
Очень поэтично написал об этом известный широко в узких кругах Arthur O’Dwyer. Его представление об этом примитиве сложилось не сразу. После послеобеденной медитации он понял, что Ленин - гриб, а once_flag - стеклянный холм.
Нет, это не художественный свист фляги, этот образ взят из детской сказки "Принцесса на стеклянной горке".
"Неподалеку от королевского дворца возвышалась высокая-превысокая стеклянная горка, скользкая, как лед. На самой вершине восседала королевская дочь, держа три золотых яблока на коленях. Тот, кто сможет взобраться и схватить эти три яблока, получит в жены ее и половину королевства в придачу".Вообще, яблоки мы можем смело выбросить. Они только портят метафору. Главное, у нас есть задача. Задачу нужно решить. Сделать это может только один человек: новообразовавшийся муж не позволит принцессе выйти замуж еще раз. Немаловажной частью концепции является возможность неудачной попытки соискателя, это нормально. Так и представляю себе очередь рыцарей у холма, с вожделением поедающих глазами яблоки. Точно, мы же их выкинули!.. тогда просто поедающих августейшие коленки. Каждый ждет своей очереди, чтоб попытаться добраться хотя бы до лодыжек принцессы, которые она уже зачем-то обильно сдобрила взбитыми сливками. А, нет, это крем для бритья. После позорного падения кандидат может повторить заход. Как только один рыцарь преуспеет, принцесса тут же возьмет несчастного в оборот, а все остальные могут расходиться. Если перенести эти странные фантазии на C++, карабканье на горку выражается в виде исполнения целевой функции (или лямбда-функции, или функтора, без разницы), а успехом предприятия можно назвать выполнение функции до конца. Падение с горки означает, что функция не завершилась, потому что было брошено исключение. Это единственный способ потерпеть неудачу. При выключенных исключениях это невозможный сценарий, выполнение формально всегда идет до конца. Ну вот, довольно филологии, мы готовы погружаться дальше в кроличью нору.
446
happy old new year
Оказывается, новогодние каникулы уже закончились! С чем и спешу поздравить всех энтузиастов своего дела, увлеченных работой таскопередвижников и иных трудоголиков-надомников. Наконец-то томными зимними вечерами будет чем заняться, а отражение в зеркале перестанет пугать. В самом-то деле, где это видано, чтоб белки глаз белые были, а щеки розовые? Должно быть наоборот.
Ну и всем нормальным людям тоже респект.
Всем спасибо, что следили за постами в прошлом году, и, может быть, даже не отпишетесь в новом. Правда, мое почтение! Я-то что, я пишу, мне это все приходится читать, а вы по доброй воле...
К несчастью, контркультурная-окололитературная стенгазета "Два с половиной креста" вновь выпустит свои пасквили.
Praemonitus, praemunitus.
446
developer's poem
Белый снег, исчадие черного вечернего неба. Падает не переставая, густо, абсолютно бесшумно. Выхваченные фонарем из тьмы, хлопья медленно оседали внутри огромного конуса света. Зрелище это невольно поднимало в душе такой же восторг, как и первое вторжение в волшебные леса Готики.
Только Пал Михалыч не полнился радостью по поводу налета белых мух. Банковский андроид, пойманный неделю назад, категорически отказывался убирать снег. Полный энергии, он взывал к Терпсихоре и рвался танцевать. "Ничего, посидит недельку без батареек, - думал Михалыч, - глядишь, по-другому запоет".
Пока работать широкой лопатой приходилось самому. Белая гора медленно росла перед избушкой, но уже вычищенные дальние уголки двора возле люфт-клозета опять покрывались свежим холодным пушком.
- Дед, заканчивай, нет смысла в такой снегопад чистить, - сказала голова внука, внезапно появившись из уличной темноты над калиткой.
- Это ничего, Мишган, просто ты в отечественном кровавом энтерпрайзе не работал, - усмехнулся Михалыч, - а то бы пересмотрел взгляды на бесполезный труд.
- Так ведь и ты не работал! Ты же сначала со стековерфлоу копировал, а потом тебе дипсик все генерил... - внучек, приоткрыв калитку, начал протискивать свое пышное тело внутрь, - Ты этот... вайб-кодер был, вот кто!
- Ты чего, пёс! Я промпт-инженер! - летящая лопата гулко бухнулась точно в забор, - да тебя еще в проекте не было, когда я на плюсах программировал, вот этими самыми руками!
Михалыч воздел кверху свои клешни и застыл, будто бронзовый древнегреческий молящийся мальчик с острова Родос.
- Хорош, нам-то не гони, все знают, что на священном языке писать человеку невозможно, - крикнул Машган откуда-то из-за забора, - да взорвется мозг того и треснет афедрон!
- Да вы все просто отупели со своими чатжипитями...
- Дед, я сейчас в фабричном магазине елочных игрушек накупил на всю бабкину пензию, на маркетплейсе продам втридорога, - голос в темноте повеселел, - а тебя на ИИ променяли, вот ты и бесишься.
- Я сам ушел. В начальники... - насупился Михалыч, погрузившись в воспоминания.
Был же когда-то он Павлом Михайловичем, уважаемым человеком. Тогда, в общем, преуспел он в управлении: набрал за еду кучу сеньоров-помидоров и веселил их батогами время от времени, чтоб лучше бегали и быстрее рожали Продукт. Да, слабаки часто не справлялись и удирали, иные не выдерживали давления административного таланта Павла Михайловича. Процессы, требования для него - суета сует и пена дней. Стандарты еще напридумывали всякие, чтоб только ему мешать трудиться. А хороший разработчик и в блокноте драйвер ядра набросает нормально. Потом, правда, разработанная станция Марс-35 в Луну врезалась, но это бывает, всего предусмотреть невозможно. Пробовали рядовых неумех на ИИ заменить, так этого стервеца даже зуботычиной не замотивируешь. А без этого какой будет результат? Вот и разорили контору всякие трутни и бездельники.
Старик, захваченный невеселыми мыслями, тяжело опустился в сугроб. Мишган опасливо приблизился и сел рядом.
Снег не спешил покоряться гравитации, он словно завис, давая полюбоваться собой в ярком свете фонаря.
- Новый Год уже скоро, - вздохнул Пал Михалыч, - надо еще курьера поймать к празднику.
- Точно, у них все деньги мира... - Мишган облизнулся, - и еда вкусная!
Всего вам доброго,
С Новым Годом!🥳
446
consteval ~D()?
Каждый окончательно решенный вопрос порождает еще несколько новых. Сегодня разработчики изнывают, нудят, как дети в магазине игрушек: "Ну дайте нам
constexpr деструктор, ну пожалуйста! Полгода сам буду память освобождать после себя, чессловооо!.." Завтра же, получив вожделенную фичу, заводят другую песню: "А чего не consteval? Вот конструкторы давно уже можно consteval делать, я как дурак с constexpr деструктором сижу!"
Нет, что-то я погнал, это уж слишком. Разработчики, все как один, разумные люди. Так ведь, товарищ Ben Craig, автор предложения P3421R0 Consteval destructors от 12 октября 2024 года?
За этим гражданином числятся и другие предложения, в которых ему очень бы пригодились чисто consteval объекты для неких специфических целей.
Приведу несколько соображений, зачем это нужно и что делать.
Допустим, есть у нас некий класс current_int, который мы хотим использовать для выражения времени компиляции
struct current_int {
int *buf = nullptr;
consteval current_int() = default;
consteval current_int(int n) {
buf = new int{n};
}
consteval void Create() { buf = new int{42}; }
constexpr void CreateConstexpr() { buf = new int{42}; }
consteval int Get() const { return 0; }
constexpr int GetConstexpr() const { return *buf; }
constexpr ~current_int() {
delete buf;
}
};
Что будет, если попытаться его применить в обычной не-consteval функции?
void test_current() {
current_int s{};
}
Это не только компилируется, но и у объекта s будет вызван деструктор! А чего вы ожидали? Хоть объект не будет создан в runtime, но деструктор-то у него constexpr, поэтому вызовется при выходе из функции test_current. К счастью, в обычной функции конструктор current_int(int n) пока вызвать нельзя из-за кучевой аллокации, также нельзя вызвать методы s.Create() или s.Get():
error: call to consteval function 'current_int::Get' is not a constant expression note: read of non-constexpr variable 's' is not allowed in a constant expressionПоэтому коллизий освобождения памяти, выделенной в
conseval контексте, можно не бояться, а освобождать nullptr можно сколько вздумается.
Конечно, возможно вызвать только CreateConstexpr() и GetConstexpr(), но это будет типичное не то, не consteval.
Вместо этого предлагается пресечь это кровосмешение, и, если объект чисто consteval, пусть таким и остается. Запретим тащить его в runtime, делов-то:
struct proposed_int {
int *buf = nullptr;
consteval proposed_int() = default;
...
consteval ~proposed_str() { // consteval!
delete buf;
}
};
void test_future() {
proposed_str s0; // ERROR!
}
Еще одно возражение против текущего положения вещей:
В constexpr деструкторе невозможно использовать consteval функции в runtime.
struct my_int {
int *buf = nullptr;
consteval void clear() {
delete buf;
buf = nullptr;
}
// constexpr не может вызвать consteval
constexpr ~my_str() {
clear();
}
};
Логично, зачем нам объект в runtime, если заранее известно, что работать он не будет?
error: call to consteval function 'my_int::clear' is not a constant expression note: implicit use of 'this' pointer is only allowed within the evaluation of a call to a 'constexpr' member functionЛучше сразу написать:
consteval ~my_int() { // proposed
clear();
}
и отрезать ему пути для отступления в иные контексты.
Идея, конечно, не лишена смысла, хотя есть сомнение, что она доберется до стандарта без изъятий.446
constexpr ~D()
Наконец-то слово "зажировка" внесено в словарь русского языка. Из года в год манул-трудяга Тимофей осенью начинал традиционную зажировку, чтоб успеть к зиме зажраться. Вот и результат. Он очень молод, но уже обогатил русский язык, чем сможет похвастать не всякий литератор. Как глаголили встарь: "Что ты сделал для хип-хопа в свои годы?"
Также в приватном разговоре со мной Тимофей рассказал, что это он внес предложение P0784R1 Standard containers and constexpr. Да, изначально оно называлось так, позже двуногие переименовали его в More constexpr containers. Зимой было скучно, хотелось чего-то необычного, например, использовать больше контейнеров в выражениях времени компиляции. Он увидел несколько очевидных препятствий на пути к этому, главное из которых - динамическое выделение памяти в контейнерах. Правда, с++20 предполагал разрешить их внутри
constexpr функций, но, очевидно, это что-то вроде костыля: operator new не получал статуса constexpr, и все, что мы выделили, нам же и убирать перед выходом из функции.
constexpr int Hello(int u) {
int * p = new int {u}; // псевдодинамическое выделение
int x = *p * 2;
delete p; // здесь нужно освободить обязательно
return x;
}
Это называется transient allocation, и это совершенно не подходит для случая с контейнерами. Там чаще всего гребут память чуть ли не в конструкторе, а отдают только в деструкторе.
Тогда для реализации концепции non-transient allocation нам нужен еще и constexpr деструктор!
Тимофей попросил Louis Dionne, Richard Smith, Nina Ranns и, конечно, Daveed Vandevoorde погладить котика и замолвить словечко в комитете:
"Ограничение, при котором деструктору нельзя быть constexpr, довольно искусственное, - писали они, - просто забудем об этом недоразумении.
Мы уже обкашляли вопросик с разработчиками MSVC++, GCC, Clang, и фронтедна EDG, и они согласны, что в худшем случае constexpr объекты станут чуть медленнее вычисляться".
Ребятки предложили новые правила для constexpr деструкторов:
Деструктор может быть constexpr. Деструктор по умолчанию неявно constexpr, если не вызывает не-constexpr деструторов. Тривиальный деструктор неявно constexpr. Литеральный тип требуют constexpr дуструктор Даже с тривиальным деструктором объект не будет доступен после уничтожения в том числе в compile-time контексте.В результате все мы получили возможность исполнять довольно сложные вещи в
constexpr. Например, в c++20 можно организовать класс ConstClass
struct ConstClass {
constexpr ConstClass(int num) {
x = new int{num};
}
constexpr ~ConstClass() {
delete x;
}
constexpr int get() { return *x; }
int *x;
};
специально для каких-нибудь compile-time вычислений:
consteval int Get() {
ConstClass x{2};
return x.get();
}
static_assert(Get() == 2);
Уберем константность деструктора и тут же получим по рукам от GCC:
error: variable of non-literal type 'ConstClass' cannot be defined in a constexpr function note: 'ConstClass' is not literal because its destructor is not constexprКонечно, даже objdump-ом вы не найдете и следа использования
operator new ли delete для функции Get().
Тимофей доволен проделанной работой, он завершил заплюсовку.446
std::uninitialized_copy
Сколько выпало снега!
А ведь где-то люди идут
Через горы Хаконэ...
Писал один восточный поэт-песенник. Вот уж сомневаюсь, что Басё этих людей жалел. В Хаконэ полно онсэнов, наверняка он просто завидовал всяким мажорам, что не спеша променируют в направлении геотермальных источников. Засядут там, мерзавцы, в горячей воде, на заснеженный бамбук любуются, рис с сырой рыбой уплетают. А тут сидишь в четырех стенах, греешься о ноутбук, и любоваться остается только функциями из группы
std::uninitialized_* для работы с сырой памятью.
Любителей велосипедов и строителей библиотек рисом не корми, дай поразмещать всякие объекты в выделенном, но неинициализированном участке памяти.
С тривиальными типами чаще всего можно не церемониться, заполнить память нулями через memset и сделать вид, что объекты созданы. Однако же очень часто типы попадаются сложные, вроде:
struct SuperClass {
SuperClass() : x {42} {}
SuperClass(int num) : x {num} {}
private:
int x;
};
Деваться некуда, надо инициализировать, нужно вдохнуть в объекты жизнь. Сделать это можно, конечно, вызовом функции uninitialized_default_construct, это все равно, что вызвать конструктор по умолчанию: new (pbuf) SuperClass; // .x == 42
Здесь pbuf - указатель на буфер, полученный относительно честным путем:
void *pbuf = get_my_buffer();
Есть еще вариант вызвать uninitialized_value_construct, который исполнит zero-initialization: new (pbuf) SuperClass {}; // .x == 0
Имеется также функция std::uninitialized_copy. Вроде бы очередная функция копирования, но не все так просто.
Допустим, есть у нас один объект сложного типа:
SuperClass sc {};
Копировать его в сырую память надо аккуратно, через placement new:
new (pbuf) SuperClass {sc};
Все производители библиотек и строители велосипедов несомненно пользовались таким приемом. Только когда имеешь дело с целым контейнером объектов, то без специальной функции не обойтись. Судя по всему этот вопрос давно будоражил беспокойные умы, поэтому подобная функция появилась аж в c++11 - std::uninitialized_copy_n.
Ее описание интуитивно понятно:
template<class InputIt, class Size, class NoThrowForwardIt>
NoThrowForwardIt uninitialized_copy_n(InputIt first, Size count,
NoThrowForwardIt d_first );
где first - начало контейнера, из которого мы копируем объекты, count - размер контейнера, d_first - начало сырого участка памяти.
Тип NoThrowForwardIt не должен вводить в заблуждение, это значит, что никакое приращение, назначение, сравнение или косвенное обращение не может выкидывать исключения.
Странно, что uninitialized_copy появилась много позже, аж в с++17. Использование ее ничем не отличается, кроме указания последнего итератора вместо размера контейнера.
Пример использования:
std::vector<int> seq {0, 1, 3, 7};
int* first {static_cast<int*>(pbuf)};
auto last = std::uninitialized_copy(std::begin(v), std::end(v), first);
Думаю, функция делает нечто такое:
for (; first != last; ++d_first, (void) ++first)
::new (voidify(*d_first))
typename std::iterator_traits<NoThrowForwardIt>::value_type(*first);
Вызывает оператор new некого типа std::iterator_traits<NoThrowForwardIt>::value_type. Если мы попробуем подсунуть pbuf (указатель на void*) вместо указателя first, то при попытке собрать увидим только грязные ругательства компилятора. Получается, что информацию про конечный тип функция берет из типа выходного итератора NoThrowForwardIt.
Раз так, то что будет, если value_type входного итератора не совпадает с выходным?
Это должно сработать, если функция реализована как мы ожидаем:
SuperClass* first {static_cast<SuperClass*>(pbuf)};
auto last = std::uninitialized_copy(std::begin(seq), std::end(seq), first);
У SuperClass есть подходящий конструктор, который принимает на вход значение типа int, поэтому uninitialized_copy создаст в памяти объекты типа SuperClass.
Такого эффекта не добиться вызовом uninitialized_fill. Да, есть и такая функция, но она инициализирует все объекты только одним значением.
Неожиданный и приятный side-effect, хотя... может быть, так и задумано.446
std::condition_variable. __gthread_cond_t
Невероятно, зима все же наступила. Сложно в это поверить, когда снега нет, и всюду трава зеленая будто во сне космонавта. Только с календарем не поспоришь. Еще меньше хотелось спорить с кошкой, успешно завершившей осеннюю зажировку. Теперь лежит и на каждого тощего двуногого глядит презрительно, а в будущее - с оптимизмом. Ведь зима - время чудес. Время, когда наивные юноши готовы привязывать свои тюбинги к любым транспортным средствам солиднее велосипеда в надежде, что из постылой холостяцкой берлоги их заберет с собой высокопоставленная снежная особа. Тогда заживут! Не придется больше печку кизяками топить и афедрон морозить в люфт-клозете. Сиди себе, валяй дурака, из льдинок неприличные слова складывай.
Однако жизнь пока предлагает составить лишь нативный тип условной переменной из буфера и мьютекса.
Циклический буфер еще может обойтись конструктором по умолчанию, а вот внутренний мьютекс придется инициализировать явным образом. Поскольку мы затащили в структуру низкоуровневый тип мьютекса
__gthread_mutex_t, определенный в этом же файле, то будем использовать предназначенную для инициализации функцию __gthread_mutex_init_func:
static inline void __gthread_cond_init_func (__gthread_cond_t *__cond) {
__gthread_mutex_init_func(&__cond->ring_mutex_);
}
Кроме того, функция вызывается в конструкторе через макрос __GTHREAD_COND_INIT_FUNCTION:
#define __GTHREAD_COND_INIT_FUNCTION __gthread_cond_init_func
При этом __GTHREAD_COND_INIT не должен быть определен. Если есть такой макрос, его следует найти и уничтожить.
Самое время вспомнить, что есть еще и деструктор:
inline int __gthread_cond_destroy (__gthread_cond_t *__cond) {
return __gthread_mutex_destroy(&__cond->ring_mutex_);
}
Все, с рутинными операциями закончили, время творчества! Или нет... займемся функцией для метода wait условной переменной:
inline int __gthread_cond_wait (__gthread_cond_t *__cond, __gthread_mutex_t *__mutex) {
TaskHandle_t current {xTaskGetCurrentTaskHandle()};
__gthread_mutex_lock(&__cond->ring_mutex);
__cond->waiting_ring.Push(current);
__gthread_mutex_unlock(&__cond->ring_mutex);
__gthread_mutex_unlock(__mutex);
vTaskSuspend(current);
__gthread_mutex_lock(__mutex);
return 0;
}
Сначала мы получаем дескриптор текущего потока current через xTaskGetCurrentTaskHandle. Затем, заблокировав внутренний мьютекс ring_mutex, добавляем current в буфер ждунов.
После освобождаем внешний мьютекс доступа к данным __mutex. Если мы заблокируем поток вместе с зажатым мьютексом, то с теми данными работать будет сложно.
Наконец, тормозим текущий поток vTaskSuspend.
Дальше current чилит, пока его не возобновят, после чего он тут же заблокирует внешний мьютекс и выйдет из функции.
Вспомним, в каком контексте мы вызываем wait:
std::unique_lock lk{m};
cv.wait(lk, []{ return ready; });
После разрушения lk, мьютекс корректно высвободится.
Думаю, понятно как построить функцию оповещения одного потока:
inline int __gthread_cond_signal (__gthread_cond_t *__cond) {
__gthread_mutex_lock(&__cond->ring_mutex_);
TaskHandle_t current{__cond->waiting_ring.Pop()};
__gthread_mutex_unlock(&__cond->ring_mutex_);
if (current) {
vTaskResume(current);
}
return 0;
}
Прикрывшись внутренним мьютексом, извлекаем из waiting_ring дескриптор ждуна. Если он ненулевой, то запускаем поток vTaskResume.
Для массового пробуждения нужна функция __gthread_cond_broadcast, где делаем все то же самое, пока наши цикл не устанет, а в буфере не останется потоковых дескрипторов.
inline int __gthread_cond_broadcast (__gthread_cond_t *__cond) {
__gthread_mutex_lock(&__cond->ring_mutex_);
TaskHandle_t current{__cond->waiting_ring.Pop()};
__gthread_mutex_unlock(&__cond->ring_mutex_);
while(current != nullptr) {
vTaskResume(current);
__gthread_mutex_lock(&__cond->ring_mutex_);
current = __cond->waiting_ring.Pop();
__gthread_mutex_unlock(&__cond->ring_mutex_);
}
return 0;
}
Вот и очередной примитив в колхозную коллекцию FreeRTOS.446
std::condition_variable. __condvar
Любопытство убило кошку, оставило Варвару калекой, а Серегу с цветами завело в трансформаторную будку. Любопытство и желание странного. По тем же причинам мы сейчас здесь. В глубине стандартной библиотеки. В недрах
std::condition_variable. По колено в коде.
Вот перед нами класс __condvar. Какие тайны скрывает он в своей тщедушной реализации?
Внутри виднеется член класса. Сидит один-одинёшенек
__gthread_cond_t _M_cond;
однако не унывает, ведь он знатного типа __gthread_cond_t. Это широко известный в узких кругах нативный тип условной переменной. Поэтому _M_cond с радостью принимают во многих методах класса. Например, в конструкторе:
__condvar() noexcept {
#ifndef __GTHREAD_COND_INIT
__GTHREAD_COND_INIT_FUNCTION(&_M_cond);
#endif
}
Правда, сработает, если только не определен дефайн __GTHREAD_COND_INIT и определен макрос функции инициализации __GTHREAD_COND_INIT_FUNCTION. Подойдет для случая более-менее сложного процесса создания __gthread_cond_t. Если же его начальное значение можно выразить единственным значением, то определяем __GTHREAD_COND_INIT, тогда назначение _M_cond выносится из конструктора:
__gthread_cond_t _M_cond = __GTHREAD_COND_INIT;
Деструктор, внезапно, уничтожает объект через вполне конкретную функцию __gthread_cond_destroy:
~__condvar() {
int __e __attribute__((__unused__)) = __gthread_cond_destroy(&_M_cond);
__glibcxx_assert(__e != EBUSY); // threads are still blocked
}
В методе ожидания засела функция __gthread_cond_wait, куда передается указатель на мьютекс вторым параметром.
void wait(mutex& __m) {
int __e __attribute__((__unused__)) = __gthread_cond_wait(&_M_cond, __m.native_handle());
__glibcxx_assert(__e == 0);
}
Методы оповещения тоже не блещут оригинальностью, используют внешние функции с говорящими названиями __gthread_cond_signal и __gthread_cond_broadcast.
void notify_one() noexcept {
int __e __attribute__((__unused__)) = __gthread_cond_signal(&_M_cond);
__glibcxx_assert(__e == 0);
}
void notify_all() noexcept {
int __e __attribute__((__unused__)) = __gthread_cond_broadcast(&_M_cond);
__glibcxx_assert(__e == 0);
}
Тут нам становится понятно, что объект __condvar - это прокси, соединяющий класс condition_variable и низкоуровневые функции __gthread_cond_*, которые еще не реализованы!
Напомню, что __gthread_cond_t тип мы уже определяли ранее в виде заглушки в заголовочном файле gthr-user.h, так же поступили со всеми __gthread_cond_* функциями. Что вполне объяснимо, их реализация будет сильно зависеть от конкретной операционной системы.
Сейчас настало время написать их для нашего freeRTOS. Начнем с типа __gthread_cond_t:
struct freertos_cond_t {
__gthread_mutex_t ring_mutex;
SimpleRingBuffer<TaskHandle_t> waiting_ring;
};
using __gthread_cond_t = freertos_cond_t;
Что-то подсказывает, что для осуществления дерзкого плана нам потребуется структура freertos_cond_t, содержащая член класса waiting_ring и внутренний мьютекс ring_mutex. Тип SimpleRingBuffer - это простейшая реализация циклического буфера, сделанная левой ногой на коленке правой. Код настолько ужасен, что не буду его показывать, но задачу свою буфер выполняет: метод Push заталкивает указатель TaskHandle_t в очередь, а Pop извлекает. Чтоб вызывать эти методы безопасным образом, нам и потребуется ring_mutex, ведь со стопроцентной вероятностью можно сказать, что вызывать их будут из разных потоков. Скоро станет ясно, почему. A suivre.446
std::condition_variable
Оказывается, при переводе рассказа нельзя свою концовку придумывать, даже если оригинальная совершенно явственно абсолютно бездарна.
Что же, после окончательного изгнания меня из литераторов, наконец-то смогу вернуться к колхозному изготовлению стандартных с++ примитивов синхронизации для freeRTOS.
Мое воображение давно будоражили картины реализации условной переменной. Я даже мысленно выстроил схему работы
std::condition_variable в потоках freeRTOS. Осталось понять, каким арсеналом средств располагает встроенный в STM32CudeIde GCC для нашей многострадальной платки STM32L4.
Во-первых, нам доступен заголовочный файл condition_variable, там задекларированы все методы класса. Во-вторых, реализация нам недоступна, ну и черт бы с ней. Как и в случае с std::thread, напишем свои собственные реализации методов.
Для начала попробуем простейший пример, где в одном потоке ожидается готовность данных:
std::unique_lock lk{m};
cv.wait(lk, []{ return ready; });
В другом потоке мы делаем вид, что готовим эти данные:
{
std::lock_guard lk{m};
ready = true;
}
cv.notify_one();
Можно и notify_all бахнуть, без разницы. Поэтому неплохо бы реализовать тройку методов: wait, notify_one, notify_all.
Внутри функций особо не разгуляешься, в состав класса std::condition_variable входит единственный член:
class condition_variable {
using steady_clock = chrono::steady_clock;
...
__condvar _M_cond;
...
Некий _M_cond сомнительного, хоть и знакомого нам типа __condvar.
Вы же помните, этот тип уже мелькал в Химках! То есть, видели его в std_mutex.h:
// Implementation details for std::condition_variable
class __condvar { ... };
По счастливой случайности внутри класса есть все что нужно для условной переменной. Тут тебе и конструктор, и деструктор, и методы wait, notify_one, notify_all.
Теперь понятно, как на этой базе написать более высокоуровневые методы.
От конструктора и деструктора мы ничего особого не ждем, можно оставить их по умолчанию:
condition_variable::condition_variable() noexcept = default;
condition_variable::~condition_variable() noexcept = default;
В метод ожидания передается ссылка на блокировку unique_lock<mutex>, а соответствующий метод __condvar хочет ссылку на mutex. Хорошо, что unique_lock способен вернуть указатель на внутренний мьютекс.
void condition_variable::wait(unique_lock<mutex>& __lock) {
_M_cond.wait(*__lock.mutex());
}
Функции оповещения вообще не вызывают никаких затруднений, просто вызываем методы _M_cond с тем же названием.
void condition_variable::notify_one() noexcept {
_M_cond.notify_one();
}
void condition_variable::notify_all() noexcept {
_M_cond.notify_all();
}
Хоть это пока не собирается, но мы почти у цели. Далее разберемся с __condvar классом. A suivre.