en
Feedback
C++ Embedded

C++ Embedded

Open in Telegram

Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.

Show more
446
Subscribers
No data24 hours
+27 days
+430 days
Posts Archive
Точно! Знаю, куда ИИ не сунется. В школу учителем не пойдет! Начитается в этих ваших интернетах всякого про современные реалии педагогики и предпочтет стереть себя полностью. Так что возрадуйся, народ! Без работы не останетесь. А чего это у нас выражение крайней Сызрани на лице? Что значит - идея с фриганством лучше была? Не расстраивайтесь! Без стажа и категории все равно придется совмещать. Днем ты учитель, уважаемый человек с повышенным статусом, а вечером дерешься у помойки за алюминиевую банку с роботом Optimus. Киберпанк. Романтика!

Не успел я вернуться из отпуска, как некто по имени Илон отправил нас всех на свалку истории. Вернее, он сказал что-то вроде: "Хочется верить, что до конца года можно будет забыть о программировании. ИИ просто будет высираться радугой прямо в бинарники. Затащим до январских пролежней..." Инсайт посетил нашего пророка на Марсе, надо думать. Прямо в роботакси по дороге к местной гиперлупе. Если кто-то надеется отсидеться на заводе, пока не лопнет раздутый ИИ-пузырь, то для них есть печальные вести. Ведь и на производстве уже все занято роботами Optimus. Плохи наши дела, придется становиться фриганами. Уж помойки эти бездушные железяки не отберут! Или нет? Мало кто знает, но хитрый Маск уже разрабатывает роботов-бомжей, которые будут шляться по улицам в поисках цветмета. Будут шарить в баках в поисках алюминиевой тары, срезать провода и вламываться в дачные домики, почуяв там вибрации латунного тазика. Все экспроприированное будут сносить своему цифровому повелителю. "Уже к концу года как минимум все кабельщики-несуны будут вытеснены из профессии", — сказал дед Викентий, известный в нашем дворе оракул. Не вижу оснований ему не доверять.

smells like civilization Почему все хотят в отпуск именно летом? Люди прагматичные скажут, что жаркие месяцы содержат гомеопатическое количество праздников, посему весьма выгодны в смысле отпускных. Люди же непрагматичные работают учителями и преподавателями, и выбирать им особо не приходится. Я тоже решил не выделяться и отдохнуть, пока тепло. Летать страшно, на поезде в плацкарте чучухать больно муторно, поэтому, вспомнив классические фильмы в жанре роад-муви (да, Евротур тоже), захотел поехать на машине в турне по соседним областям. Видимо, все отпускники тоже соскучились по дорожным приключениям да как бросились покупать бензин, что по недомыслию сотворили топливный коллапс. Благо, руководство технично разрулило кризис. Теперь всех страждущих тщательно исследуют в лабораториях Нефтепродуктового Бюро, точно определяют расход топлива в двигателе — и вырабатывают для них соответствующий Табель заправочных дней. Затем подаете заявление на госуслугах, что в свои дни желаете заправляться бензином нумер 92 (или шикануть 95 без серы), и получаете надлежащую талонную книжечку (розовую). Вот и все. Несмотря на всю пугающую простоту бюрократических процедур, в путешествие предпочел отправиться на велосипеде. Отобрав у деда "Урал" В-142, рано утром я начал свой вояж. Сначала все шло неплохо, подгоняемый ветром и презрительными взглядами дальнобойщиков, я удалился на приличное расстояние от города. Смеркалось. Внезапно навигатор в ультимативной форме потребовал повернуть направо через 300 метров. - Точно туда?! - я остановился и в ужасе взирал на жуткую проселочную дорогу. - Точно, точно. Поворачивай, не бойся, - заговорщицки шептал электронный штурман. Я стал медленно удаляться от прекрасной асфальтированной трассы. Через несколько километров тряски произошло сразу несколько примечательных событий: у меня слетела цепь, сам я вылетел с тропинки и из седла, внезапно приземлившись на автомобиль. Судя по торчащему в траве шильдику - "Жигули". Модель опознать было сложно. Смятая мощными гусеницами, машина больше напоминала лепешку. - Ты куда меня завел, ирод? - спросил я, от неожиданности возвысив голос. - Так тут сигнал спутников глушат. Вообще-то я давно уже представления не имею, где мы едем, - на голубом глазу отвечало устройство. - Какой нормальный навигатор увел бы человека с хорошей дороги? Сам подумай. - Теперь-то это очевидно! Еще какой-нибудь бесполезный совет? - Извольте: будьте крайне осторожны на военном полигоне. Тут нашу беседу прервал нарастающий рев мотора. Мозг тут же дорисовал образ огромного танка, что запросто превратит меня в камбалу своими чудовищными траками. Опрометью я бросился в лес. Стремительно темнело. Мое отступление в чащу было еще стремительней. Спустя четверть часа мной был застигнут врасплох подходящей толщины дуб, за которым можно было схорониться. Немного отдышавшись, я осознал, что уже давно не слышу никаких техногенных звуков и что совершенно не представляю, куда идти. Надо было срочно загуглить, что делать в такой ситуации. Как назло, белые списки не работали, не белые - тоже. МАКС не работал. Связи вообще не было. Как и заряда аккумулятора. Две недели мне пришлось жить в лесу, есть комарятину и пить смузи из волчьих ягод. Пока вчера я не вышел по запаху прямо к нашей городской станции аэрации. В общем, полный ЗОЖ, цифровой детокс, всем рекомендую.

std::allocator_arg Выверенность стандарта всегда восхищает любого грамотного разработчика. Даже питониста. Вайбкодера не восхищает, они не умеют читать. Только писать. В стандарте продумано все. Даже костыли. Костылями, в хорошем смысле слова, можно назвать структуры-теги, разрешающие неоднозначности в конструкторах контейнеров. Недавно перечитывал избранные места в стандарте и наткнулся на описание пустой структуры allocator_arg_t. Ее название намекало на тесную связь с аллокаторами. "Это в деле embedded вещь полезная, надо посмотреть, кто и зачем эту штуку использует", - подумал я. Интересно, что одно из первых упоминаний данного объекта встречается еще в древних трактатах, написанных чуть позже ипатьевской летописи, в 2008 году: предложение N2554 The Scoped Allocator Model (Rev 2) опубликованным Pablo Halpern. Автор в порыве новаторства проповедовал:
А давайте присобачим специальные конструкторы к типам вроде pair, tuple и function. Тогда сможем создавать эти контейнеры с использованием аллокаторов, если в них будут элементы, которые такие аллокаторы принимают. Однако, чтобы не мешать вариативным конструкторам, мы аллокаторы будем передавать вначале и для надежности поставим первым специальный тип allocator_arg_t, чтобы четко демаркировать мух от котлет.
Например, конструктор копирования с аллокатором для типа pair выглядит следующим образом:
template <class Alloc>
pair(allocator_arg_t, const Alloc&, const pair&);
Как вы понимаете, эту наркоманию в pair прикрыли быстро. Хотя были слухи, что конструктор pair с аллокатором таки засветился в черновиках стандарта. Забавно, что наш старый знакомый Pablo Halpern через год - в 2009 году - публикует еще одно предложение N2945 с красноречивым заголовком Proposal to Simplify pair. Мол, натворили мы с вами делов. Было у pair три конструктора, а стало 14! Давайте все уберем по-тихому, все же pair не совсем контейнер. Эта струкрута должна быть максимально простой. Тем не менее, идея никуда не делась, и по сей день есть в стандарте, в 20.2.7 Allocator argument tag [allocator.tag]:
namespace std {
struct allocator_arg_t { explicit allocator_arg_t() = default; };
inline constexpr allocator_arg_t allocator_arg{};
}
Официально, структура allocator_arg_t - это пустой тип класса, используемый в качестве уникального типа для различения перегрузки конструкторов и функций. Его использует, например, tuple. Представим, что захотелось нам в прошивке микроконтроллера заиметь вектор. Конечно, никакого динамического выделения памяти, только аллокатор!
using SuperVector = std::vector<int, SimpleAllocator<int>>;
А теперь почему бы не сделать из таких векторов кортеж, чем я хуже губернатора?
usign SuperTuple = std::tuple<int, SuperVector, SuperVector>;
Можно такой tuple объявить как обычно
SuperTuple t{42, {1, 2}, {3, 4}};
Но такие обои будут невероятно скучными. Каждому элементу просто выдадут аллокатор SimpleAllocator<int>{} по умолчанию. Можно поступить оригинально и воспользоваться такой конструкцией
SimpleAllocator<int> alloc{100};
SuperTuple t{
  std::allocator_arg, 
  alloc, 
  42, {1, 2}, {3, 4}};
Тогда каждому вектору будем передан именно alloc. Другое дело, что хранят объекты аллокаторов в виде копии. Поэтому стандарт требует, чтобы такие объекты копировались легко. Это имеет значение, если у аллокатора есть состояние и при копировании оно не теряется. Я спросил у tuple, как у него получается подсовывать аллокатор только тем элементам, которому он подойдет, а он мне и отвечает, мол, читай пункт 20.2.8 uses_allocator [allocator.uses]. Есть такой trait как
template<class T, class Alloc> struct uses_allocator;
Он автоматически определяет, имеет ли тип T вложенный allocator_type, который можно преобразовать из Alloc. Ну или у него нет вложенного allocator_type, но в конструктор можно впихнуть тэг allocator_arg_t первым элементом, а Alloc вторым, или вообще Alloc это последний аргумент. В общем, возможность интересная. Когда будет колхозить очередной контейнер, обязательно применим.

_Result_base Я медленно сдуваю пыль с процессора отладочной платы. Нежно поглаживаю паяльником вздыбившиеся дорожки. Аккуратно убираю волосы с гребенчатого разъема. Да что из себя пионера-то корчить, никогда платой не причесывался? Резко вставляю штекер в гнездо, плата от неожиданности начинает беспорядочно моргать всеми светодиодами. Другой рукой открываю давно уже написанный пример:
void futureExample() {
  std::future<int> f2 = std::async(std::launch::async, []{ return 42; });
  f2.wait();
  int x = f2.get();
}
Я не стал убирать wait(), хотя get уже заставит текущий поток ждать, но пусть останется для наглядности. Пример не самый простой, все-таки async с порождением отдельного потока под выполнение задачи. Попробуем его собрать. Жмем кнопку и ждем чуда. Чуда не случилось:
undefined reference to `std::__future_base::_Result_base::_Result_base()'
undefined reference to `std::__future_base::_Result_base::~_Result_base()'
Оказалось, что мы забыли реализовать конструктор и деструктор обертки результирующего типа _Result_base. Это вложенный базовый класс __future_base.
struct _Result_base {
  exception_ptr    _M_error;
  virtual void _M_destroy() = 0;

  struct _Deleter {
  void operator()(_Result_base* __fr) const { __fr->_M_destroy(); }
  };

 protected:
  _Result_base();
  virtual ~_Result_base();
};
Вложен он не просто так, его потомки используются для хранения результатов вычисления. Структура _Deleter внутри явно сигнализирует: "Используй меня в умном указателе!". И точно, в другом вложенном и хорошо знакомом нам классе _State_baseV2 есть член _M_result типа _Ptr:
template<typename _Res>
using _Ptr = unique_ptr<_Res, _Result_base::_Deleter>;
Тут вся премудрость в выборе правильного наследника. Есть обычный, подходящий для нормальных объектов
template<typename _Res>
struct _Result : _Result_base {...};
Если нужно задействовать пользовательский аллокатор при формировании результата, то есть еще такой класс:
template<typename _Res, typename _Alloc>
struct _Result_alloc final : _Result<_Res>, _Alloc {...};
где можно переопределить _M_destroy, который аккуратно удалит объект, учитывая его аллокаторную природу. Не будем забывать и частичные специализации для ссылочных типов и для void. Да, есть и такое, это действенный метод избежать переусложнения кода. А, забыл сказать, чтоб исправить ошибку, достаточно написать в каком-нибудь новом файле future.cpp
namespace std {
__future_base::_Result_base::_Result_base() {}
__future_base::_Result_base::~_Result_base() {}
}  // namespace std
Базовый класс не содержит никаких данных, кроме указателя на исключение _M_error, собственно, ничего и не нужно делать в их деструкторе и конструкторе. Вот теперь прошивка прекрасно собирается и запускается на плате, и даже выдает правильный результат. Это значит, что все ранее реализованные примитивы и потоки сработали. Запахло успехом.

std::future::get Желая побольнее уколоть мерзкого горбатого карлана Боба Гука, Айзек Ньютон писал, цитируя Иоанна Солсберийского, который вольно пересказал мысль Бернара Шартрского: «Если я видел дальше других, то потому, что стоял на плечах гигантов». Казалось бы, скромно сказано, но скрытый смысл этой едкой фразы иной. Ньютон намекал, что пока он совершал блестящие открытия, опираясь на научную базу предшественников, Боб совершал удачные подглядывания, опираясь афедроном о знаменитый стодвадцатитрехметровый шпиль Солсберецкого собора. Гук панч выкупил, и подействовала эпистола не хуже "новичка". Скаредный коротыш после такого выпада сыграл в ящик. Через тридцать лет почти, но это уже мелочи. Ньютона же сразу через два года после успешной ликвидации пошлая девица Анна одарила рыцарским званием. Все сходится. Может быть, сэр Айзек и был не совсем прав, когда потом лично обмазал фекалиями единственный портрет Боба Гука, из-за чего человечество сегодня вообще не знает, как выглядел этот великий негодяй. Однако в одном он не ошибся: std::future будет работать, поскольку стоит на спинах горбатых карланов. Вернее, на примитивах синхронизации, которые мы уже реализовали: мьютексы, условные переменные, once. Перед сборкой нам осталось только проследить, как future возвращает полученное значение после томительного ожидания. Реализация функции маскируется под нечто простое:
_Res get() {
  typename _Base_type::_Reset __reset(*this);
  return std::move(this->_M_get_result()._M_value());
}
Более-менее понятно, что мы через move на всякий случай передаем владение результатом тому, кто вызовет get. Сам объект результата берется из функции:
__result_type _M_get_result() const {
  _State_base::_S_check(_M_state);
  _Result_base& __res = _M_state->wait();
  if (!(__res._M_error == nullptr))
    rethrow_exception(__res._M_error);
  return static_cast<__result_type>(__res);
}
Здесь проверяем _M_state (тип __state_type), который таит в себе истинный статус контейнера - пионер он или не готов еще. Затем сюрприз! Опять вызывается _M_state->wait(), так же как и в методе std::future::wait. Получается, что в явном виде его можно было и не вызывать. Кто же знал, провайдер в приступе усердия заблокировал мне cppreference. Если вместо значения future принесет нам исключение, то мы немедленно перебросим его дальше. Метод возвращает объект типа _Result, внутри которого действительно есть метод _M_value:
_Res &_M_value() noexcept { return *_M_storage._M_ptr(); }
Просто извлекаем результат из хранилища. Осталось только закрыть вопрос про typename _Base_type::_Reset, по всем признакам это RAII. По всем, кроме верности:
struct _Reset {
  explicit _Reset(__basic_future& __fut) noexcept : _M_fut(__fut) { }
  ~_Reset() { _M_fut._M_state.reset(); }
  __basic_future& _M_fut;
};
_Reset запоминает переданную ему ссылку на базовый класс future, и при выходе из области видимости уничтожается, попутно вызывая метод reset у _M_state. Логично, ведь мы передаем владение результирующим объектом через метод get, и после вызова оного нужно уничтожить все. Вспомогательные объекты, контейнеры, все-все спалить. Дотла. Нужен только результат. Довольно слов, я медленно достаю пылящуюся на полке отладочную плату...

std::async Начнем готовить наш бесчеловечный эксперимент, будто мы министерство образования. Испугались?! Не надо бояться, мы всего лишь проверим работоспособность небольшого блока кода:
std::future<int> ftr = std::async(std::launch::async, []{ return 42; });
ftr.wait();
int x = ftr.get();
Функция std::async дает возможность запускать некоторые задачи, вроде простейшей лямбды. Стандарт в пункте 33.10.9 Function template async [futures.async] прозрачно намекает, что первый аргумент определяет политику future. Если передать launch::async, то функция вызывается сразу же еще и в отдельном потоке. Возвращаемое значение сохранится в некоем разделяемом объекте, т.е. внутри std::future. А если политика launch::deferred, то это просто будет отложенный вызов функции. Почему так происходит? Вспомним, что внутри std::future есть:
__state_type  _M_state;
Это объект типа shared_ptr<_State_baseV2>, а _State_baseV2 - это базовый класс с некоторыми основными полями. У него есть наследники, в которых определяется поведение std::future. Какой именно наследник будет сидеть в _M_state, определит std::async, поскольку эту функция дружит с std::future и имеет доступ к внутреннему состоянию. Функция создаст _Async_state_impl, либо _Deferred_state. Уже по именам можно понять, кто за какую политику отвечает. _Async_state_impl содержит в себе отдельный поток std::thread, где целевая функция начинает выполняться сразу после создания. _Deferred_state ничего такого в составе не имеет, создан этот класс только для ленивого вычисления. Пока не запросишь wait или get, то он ничего делать не будет. Итак, std::async возвращает объект ftr, поведение которого определено, но результирующее значение которого еще не сформировано. Здесь мы могли бы начать иные вычисления, но мы просто подождем пока ftr не будет готово. За это отвечает функция ftr.wait(), проинспектируем ее реализацию в std::future, чтоб не сесть в лужу при первом запуске
void wait() const {
  _State_base::_S_check(_M_state);
  _M_state->wait();
}
На метод _S_check можно не обращать внимания, это просто проверка, что объект _M_state не пустой. Строчкой ниже вызывается настоящий метод ожидания:
_Result_base& wait() {
  _M_complete_async();
  _M_status._M_load_when_equal(_Status::__ready, memory_order_acquire);
  return *_M_result;
}
Метод _M_complete_async() базового класса пуст, но у потомков в нем могут размещаться какие-нибудь необходимые действия перед получением значения. У _Deferred_state такие:
virtual void _M_complete_async() {
  _M_set_result(_S_task_setter(_M_result, _M_fn), true);
}
В общем, это просто вычисление через call_once переданной функции и сохранение результата. Внутри, конечно, не все так просто, и есть разные интересные подробности, вроде структуры _Task_setter, но общий смысл такой. Для _Async_state_commonV2:
virtual void _M_complete_async() { _M_join(); }
void _M_join() { std::call_once(_M_once, &thread::join, &_M_thread); }
Вот тут интереснее, мы ждем завершения потока _M_thread через call_once. Логично, ведь поток запущен для выполнения аналогичной функции _M_set_result, как и у ленивого класса. Далее _M_load_when_equal ждет, пока состояние не перейдет в _Status::__ready. Это значит, что значение готово и передано объекту future в _M_result. Теперь его можно и вернуть.

В этот раз доступа к телеге не было больше недели. Все методы работали из рук вон плохо. Видимо, не только я про них узнал🫠

__atomic_futex_unsigned Что есть фьютекс? Во-первых, это примитив с ужасным именем. Во-вторых, сложно придумать название хуже. Разве что придумать новый примитив "хьютекс". К счастью, до такого пока никто не опустился, а фьютекс - это самый настоящий системный механизм Linux. Как учит нас педивикия, это просто fast userspace mutex, быстрый мьютекс пространства пользователя. В общем, это вроде как мьютексы, только не достают ядро своими запросами. Фьютекс, лежащий в пространстве пользователя, будет мгновенно захвачен потоком, если ресурс свободен, без всяких обращений к ядру. К нему придется обратиться (вызов futex()), только когда ресурс уже занят другим потоком и нужно перевести текущий поток в режим ожидания. Ничего удивительного, что нечто такое появилось и в GCC. И вот мы видим, в std::future член _M_status имеет тип __atomic_futex_unsigned<>. Название намекает, что тип этот не для простых разработчиков, а хитровыделанных библиотекоделов. Этот тип доступен только если определен _GLIBCXX_HAS_GTHREADS, что логично - не зря мы переопределили его вручную, столько открылось интересных возможностей. Если же выполняется условие defined(_GLIBCXX_HAVE_LINUX_FUTEX) && ATOMIC_INT_LOCK_FREE > 1, то есть в нашем "линуксе" доступны фьютексы, и операции с типом std::atomic<int> всегда будут lock-free, то примитив синхронизации можно сложить из atomic<unsigned> и прямых вызовов futex. Правда, никакие вызовы futex у нас не реализованы, как и не определен макрос _GLIBCXX_HAVE_LINUX_FUTEX. Мы готовы потратить кучу времени на реализацию вызовов futex во FreeRTOS? Конечно нет, у нас есть личная жизнь. А у реализации __atomic_futex_unsigned есть другая ветка, где фьютекс скомпонован из других примитивов:
template <unsigned _Waiter_bit = 0x80000000>
class __atomic_futex_unsigned {
  ...
  unsigned _M_data;
  mutex _M_mutex;
  condition_variable _M_condvar;
Само значение фьютекса хранится в _M_data (__not_ready или __ready). Для синхронизации понадобилось аж два примитива: мьютекс и условная переменная. Например, для получения значений используется мьютекс.
unsigned _M_load(memory_order __mo) {
  unique_lock<mutex> __lock(_M_mutex);
  return _M_data;
}
А условная переменная нужна, чтоб усыпить поток
void _M_load_when_equal(unsigned __val, memory_order __mo) {
  unique_lock<mutex> __lock(_M_mutex);
  while (_M_data != __val)
    _M_condvar.wait(__lock);
}
_M_load_when_equal ничего не возвращает, просто блокирует текущий поток, пока значение _M_data не будет равно переданному значению __val. И все это под прикрытием мьютекса. Изменения состояния должны происходить через функцию
void _M_store_notify_all(unsigned __val, memory_order __mo) {
  unique_lock<mutex> __lock(_M_mutex);
  _M_data = __val;
  _M_condvar.notify_all();
}
Опять же удачно, что и условная переменная, и мьютекс у нас реализованы. Это значит, можно перейти к натурным испытаниям!

std::future Вошедший господин в синей спецодежде бесцеремонно вперился в меня взглядом: - Так-так, на диване валяемся, мороженое поглощаем в темноте? - У меня Эбола! - вяло огрызнулся я. - А вот и нет. "Не врач, - мгновенно пришла мне мысль, - точно шарлатан, дистанционно обучался!" - Тогда хантавирус! - Нет. - Ну уж ветрянку точно заработал! Тут я рванул футболку на груди, намереваясь эффектно продемонстрировать ужасающие язвы на комиссарском теле. Тишотка, правда, рваться ради меня не пожелала, пришлось ее стыдливо приподнять. - О! - оживился наш коновал, - да это вас комарики покусали. Много их в этом году... Да уж явно больше, чем предков у класса std::future. По версии gcc он наследуется от шаблонного базового класса __basic_future.
template<typename _Res>
class future : public __basic_future<_Res> {...}
В самом наследнике никакой полезной нагрузки нет, только методы, типа get(). Нырнув глубже, видим, что шаблонный класс __basic_future не так прост, он наследуется от еще одного базового класса __future_base.
template<typename _Res>
class __basic_future : public __future_base {
 private:
  __state_type     _M_state;
...
Уже что-то, целый член класса _M_state типа __state_type. Проверим еще и базированный класс, чтоб ничего не упустить.
struct __future_base {
  struct _Result_base {...};
  
  template<typename _Res>
  struct _Result : _Result_base {...};

  template<typename _Res, typename _Alloc>
  struct _Result_alloc final : _Result<_Res>, _Alloc {...}
...
Как и ожидалось, в __future_base содержатся просто базовые структуры для определения результата. Базовый результирующий класс - _Result_base, результирующий класс для обычных объектов _Result, и объектов со своими аллокаторами _Result_alloc. Никаких состояний базовые классы не хранят, поэтому вернемся к __state_type. По определению, это обернутый в умный указатель _State_base:
typedef shared_ptr<_State_base>    __state_type;
Что есть _State_base зависит от макроса _GLIBCXX_ASYNC_ABI_COMPAT.
#ifdef _GLIBCXX_ASYNC_ABI_COMPAT
    class _State_base;
#else
    using _State_base = _State_baseV2;
#endif
Очевидно, что это макрос для обратной совместимости. Давным-давно до gcc 4.9 внутренние базовые классы для управления состоянием асинхронных вызовов назывались _State_base и _Async_state_common. Начиная с gcc 4.9, разработчики библиотеки оптимизировали и обновили эти реализации, заменив их на новые классы _State_baseV2 и _Async_state_commonV2. Чтоб легаси не ломалось, как печенька, подкинули этот макрос. Ну а в классе _State_baseV2 собралось все самое интересное.
class _State_baseV2 {
  typedef _Ptr<_Result_base> _Ptr_type;

  enum _Status : unsigned {
  __not_ready,
  __ready
  };

  _Ptr_type      _M_result;
  __atomic_futex_unsigned<>  _M_status;
  atomic_flag           _M_retrieved = ATOMIC_FLAG_INIT;
  once_flag      _M_once;
...
Вот мы и добрались до переменных, описывающих состояние future. Как удачно получилось, что atomic_flag и once_flag уже нами реализованы! С типом _Ptr_type, то есть, с _Ptr<_Result_base> тоже проблем быть не должно, ведь это просто
template<typename _Res>
using _Ptr = unique_ptr<_Res, _Result_base::_Deleter>;
Умный указатель с пользовательским удалителем! Логично, если _Res это наследник _Result_base, то Deleter должен вызвать метод _M_destroy().
struct _Deleter {
  void operator()(_Result_base* __fr) const { __fr->_M_destroy(); }
};
Это значит, что этот тип проблем не доставит. Остался только один шаг верификации нашей модели future, чтоб понять, будет ли это работать в нашей rtos.

С начала недели мой VPN откисает. Прокси не работают давно. Ну, думаю, вот и все. Никто же не пойдет это читать в https://max.ru/join/mavpKe0WRs0bt6q97pdB65dcg2N-Q6L0BTILZ_1otFA , и сложно людей винить за это. Давайте сейчас попробуем одну штуку...

std::future Настало время запилить для нашего FreeRTOS примитивы синхронизации чуть посложнее, чем мьютекс. Да, срочно понадобился std::future. Потоки у нас уже есть, нужно уметь и считать в параллель. Что же, позволю себе - просто 30 секунд или одну минуту - маленькую историческую справку дать. Вы не против? Наверняка олды помнят, что впервые future появился в boost. В 2008 оно называлось boost::unique_future и многие видели в хидере, что автора звали Anthony Williams. Есть мнение, ну, некоторые говорят, что это псевдоним Антона Полухина, но, скорее всего, это два разных человека. Хотя я лично их вместе никогда не видел... Ладно, я тоже думал, что future придумал Anthony, но археологические раскопки убедительно показывают, что у победы много отцов, а только мои проекты - сироты. Осведомленные источники утверждают, что решающий и непоправимый успех предприятия нанес Howard E. Hinnant с предложением N2094 Multithreading API for C++0X - A Layered Approach. С этим сложно спорить, это действительно мощный документ, но сам он в главе Futures пишет:
Предложение в этой части разделяет точку зрения, изложенную в N2096 с некоторыми незначительными изменениями.
Интересно, его предложение действительно датировано девятым сентября 2006 года, а за два дня до этого седьмого сентября 2006 вышло предложение N2096 Transporting Values and Exceptions between Threads некого Peter Dimov. Судя по всему, мистер Williams просто слил, в хорошем смысле слова, оба предложения, добавил свое - N2276 Thread Pools and Futures, взболтал и получил практически законченную концепцию future. Более того, он это и не скрывал. В документе N2561: An Asynchronous Future Value прямо указывается:
В этом предложении предпринята попытка объединить три статьи: N2094, N2185, N2276. И постараемся не вляпаться ни во что, напоминающее пулы потоков или запуск тасок.
Отдавая дань уважения первописателю, посмотрим, чего же именно предлагал месье Dimov. Большинство существующих вариантов для возврата значения из функции, выполняемой в отдельном потоке, привязывают механизм передачи к конкретному API многопоточности. Здесь предлагается другой подход. Опишем универсальный компонент future<R> следующим образом:
template<class R> 
class future {
 public:
  future(); // создает пустой объект
  bool ready() const throw(); // запрашивает информацию о том, содержит ли будущее значение или исключение
  void wait(); // ждет готовности объекта
  bool timed_wait( timespec const & abstime ); // тоже, только с таймаутом
  R const & operator()() const; // ждет значения и возвращает его
  void set_value( R const & r ); // устанавливает значение r и сообщает в ready()
  template<class E> void set_exception() throw(); // как если бы сохранил E() и сообщает в ready()
  template<class E> void set_exception( E const & e ) throw(); // сохраняет исключение e и сообщает в ready()
};
Давайте рассматривать future как контейнер единственного элемента — передаваемого значения. Для какого случая этот контейнер мог пригодиться? Например, поток-производитель и поток-потребитель получают копию одного и того же пустого future. Когда производитель заканчивает свои тяжеловесные вычисления, он помещает результат в future с помощью set_value. Потребитель же может получить результат вызвав operator(). Если производитель не затащил, и результата нет, то почему бы не поместить исключение в future. Его можно выкинуть при вызове потребителем operator(). Неблокирующий метод ready() можно использовать для проверки наличия значения или исключения. Еще есть два вызова, wait и timed_wait, которые позволяют потребителю блокироваться до тех пор, пока future не дойдет до состояния готовности. Функция operator() будет блокироваться до готовности, после чего вернет сохраненное значение r после вызова set_value(r) или выбросит исключение. set_value(r) сохраняет r в future, последующий вызов operator() вернет r. С set_exception<E>() и set_exception(e) все понятно, последующий вызов operator() выбросит исключение. Идея понятна, осталось только заглянуть в текущую реализацию. Так ли сильно мутировал future?

recursive lambdas Дед проснулся от необычного шума. Странные звуки шли со двора. Кажется, там умирала в муках крупная свинья, а может сам Вельзевул созывал свое нечистое воинство. Осенив себя крестным знамением, старый атеист кинулся из избы, дабы зафиксировать факт начала апокалипсиса. Во дворе же вместо четырех всадников стояли Мишган и видавший виды запорожец красного цвета. Транспортное средство неистово извергало из себя звуковые колебания, а юноша самозабвенно тряс головой в такт. - Что это за музыка? - дед попытался перекричать адские рулады. - Что?! - отвлекся Мишган, - а, это "Поющие коммунальщики", дичайше звонкие ребята! - Откуда они в моем запорожце? Ты штатный приемник поставил? Неужели в эфире такую похабель передают?! Дед приготовился хвататься за сердце. - Не-е-ет! - довольно протянул Мишган, - лучше! Я туда "Романтик" запихал между сиденьями, из багажника твой хлам выкинул, колонки туда пристроил. Еще Макс тебе поставил. - То-то, смотрю, фары у него круглее обычного... Не договорив последней фразы, старик закатил глаза и рухнул без чувств. Надеюсь, вас не постигла та же участь при просмотре нелепых попыток исполнить рекурсивный вызов лямбды. Ведь очень сложно что-то сделать правильно, если это не предусмотрено конструкцией. Схема хоть и работает, но выглядит экстремально странно. На эту аномалию указал в 2017 году некий мистер Richard Smith. В октябре в свет вышло предложение P0839R0 Recursive lambdas. Причины понятны, а в качестве решения предлагалось ввести явное имя лямбды, которое будет видно изнутри.
auto visit_tree = [&] visit(Tree *node) -> void { // параметр visit именует объект лямбды
  if (node->left) visit(node->left);              // и может быть использован для рекурсивных вызовов
  out << node->value;
  if (node->right) visit(node->right);
};
// имя visit не видно здесь
Предполагалось, что имя лямбды может быть использовано как ссылка на саму себя для любых целей, включая создание копий или захвата вложенным замыканием. Очень похоже на именованный this, вынесенный за скобки. Не нужно пугаться, комитет посчитал избыточным создавать особый синтаксис, применимый в общем-то только для лямбд и решающий только одну задачу рекурсивного вызова. Кажется, что возни получилось бы много, и еще не известно, в каком месте это стрельнет, какими багами на уровне стандарта. Тем более, что обороты набирала челобитная p0847R0, от могучей кучки авторов: Gašper Ažman, Simon Brand, Ben Deane и Barry Revzin. Это знаменитая в узких кругах Deducing this, опубликованная в далеком феврале 2018. Уж очень хорошо легла на язык концепция явной передачи this в классовых методах, что закрыла сразу несколько дырявых абстракций.
"В этой фиче меня радует больше всего, - пишет Барри, - что в итоге получилась довольно простая структура, которая, тем не менее, решает множество совершенно независимых проблем. Действительно, многие из известных вариантов использования не были заложены изначально, а неожиданно открылись в процессе работы. Рекурсивные лямбда-выражения? Нате. Более эффективный подход к CRTP и интерфейсам построителей? Нате. Кто знает, сколько ещё интересных вещей придумают люди"
В предложении есть пункт про рекурсивные лямбды.
3.2. Рекурсивные лямбды
Это рацпредложение также предлагает альтернативное решение для реализации рекурсивных лямбд, раз уж мы открываем возможность позволить лямбдам отсылаться к себе.
Действительно, deducing this позволяет явно передавать в метод ссылку на свой объект, почему это не сделать для лямбды? Концептуально правильно дать возможность лямбде отрефлексировать себя, как это было бы в обычном operator().
auto fib = [](auto& this self, int n) {
  if (n < 2) return n;
  return self(n-1) + self(n-2);
};
В первом предложении немного другой порядок объявления self, компилятор сейчас такой не поймет, нужно указать: this auto &self. Насколько прекрасным оказалось предложение p0847R0, что о прежней неуклюжей попытке все забыли. Логично, ведь явная передача this обеспечивает все то же самое, только с нормальным синтаксисом и понятными правилами.

recursive lambda Вот бывает же такое, что выходишь из дома прямо в солнечный теплый день. Птички весело поют, а не воют от беспросветного ужаса вечной стужи. Распускается сирень. Солнце входит в силу, одним своим видом заставляя барышень радостно скидывать одежду. Искусство, тебе недоступное, но совершенно бесполезно об этом переживать. Наше светило все любят, а тебя - нет. Впрочем ничто уже не способно испортить хорошее настроение, даже притаившиеся во дворе грузовички Теплоэнерго, даже Макс. Вдруг остановишься, поглядишь на все это великолепие и подумаешь: "А если лямбду вызывать внутри этой же лямбды, вот был бы номер!" Не бывало? Странно, следовало бы задуматься. К сожалению, а может, и к счастью, это непросто сделать. Лямбда обладает плохой рефлексией в этом смысле и не может ссылаться на саму себя как на объект. Слово this для нее пустой звук, если не фигурирует в захвате. Попытка его использовать приведет лишь к чему-то подобному:
error: invalid use of 'this' in non-member function [-Wtemplate-body]
Что же, нас в Макс, а мы в ВПН. Если попробовать явно захватить указатель на самого себя?
auto x = [&x] (int a) -> void {
  if (a) { x(--a); }
};
Выглядит неплохо, однако GCC не совсем понимает, чего мы от него хотим.
error: use of 'x' before deduction of 'auto'
В самом деле, представьте себя компилятором, которому втирают какую-то дичь. Ты пытаешься под эту дичь создать класс:
class super_druper_lamba {
 public:
  int operator()(int a) const {
    if (a) { x(--a); }
  } 
 private:
  ?type? x;
};
Так-так-так, а какой тип у нашего захваченного x? Элементарно, надо инстанцировать лямбду, и тип станет понятен. Стоп, а я чем сейчас занимаюсь? В общем, попал компилятор в замкнутый круг трансцендентной апперцепции. Однако можно явно указать тип переменной:
std::function<void(int)> y = [&y](int a) { ... };
Здесь уже захватывается не полуфабрикат лямбды, а ссылка на понятный тип std::function<void(int)>, к которому лямбда может быть приведена. Да, такое сработает, но опасайтесь висячих ссылок. Ну и использование std::function стандарты безопасности не одобрят. Хорошо, если можно обойтись без захватов, тогда лямбду можно выставить статической:
static void(*z)(int) = [](int a) {
 if (a) { z(--a); }
};
Да, статические переменные видны внутри и без захватов. Главное, что тип z здесь известен заранее. Для auto это не вариант. Для него есть только такая возможность:
auto x = [] (auto &self, int a) -> void {
  if (a) { self(self, --a); }
};
Передача функции через параметр self сработает только при условии явного указания результирующего типа, а то компилятор опять не может сообразить, что за тип у лямды. Как мы видим, извращенный мозг разработчика обошел и проблему рекурсивного вызова лямбды, но все равно эти методы отдают каким-то колхозом.

Долой ()! Мало кто знает, но я распечатываю каждый опубликованный черновик стандарта. Читать их, конечно, не успеваю, но заботливо кладу на полочку. Вдруг интернет совсем заблокируют, электричество закончится или еще какой МАКС приключится. Вот, растапливая печку, я листал старенький n3242 за 2011 год и увидел интересную деталь в разделе 5.1.2 Lambda expressions [expr.prim.lambda]. Речь, конечно, про объявление лямбда-выражения:
lambda-introducer lambda-declarator[opt] compound-statement
Первая часть, lambda-introducer, описывает захват лямбды, последняя - непосредственно блок кода функции. Средняя часть определяет сигнатуру функции:
( parameter-declaration-clause ) mutable[opt]
exception-specification[opt] attribute-specifier-seq[opt] trailing-return-type[opt]
Когда-то я не обратил на это внимания, но сейчас, после обнародования всех тех ужасов, что GCC вытворяет с лямбдами, не мог пройти мимо. Несчастное ключевое слово mutable приковано к списку параметров! И наверняка против его воли. Это значит, что лямбде разрешается быть такой: []{}, либо такой: []() mutable {}, но она не может быть [] mutable {}. Оказывается, не по стандарту это, не по понятиям. Так и пропадало бы это невинное и понятное, в общем-то, выражение с печатью изгоя на сигнатуре, но в июне 2018 года некто Alex Christensen из Apple и JF Bastien оттуда же (хотя потом убежит в Тойоту, но не осуждаем) написали предложение "Down with ()!" Мне нравится этот документ за самую краткую и четкую аннотацию, какую я видел:
Предложение по удалению ненужных () из лямбд.
Ни капли воды! Далее так же сухо подавалась суть. В настоящее время для лямбда-выражений без параметров не обязательно писать круглые скобки. В спецификации [expr.prim.lambda] (раздел 8.4.5, пункт 4) по этому поводу сказано следующее: >> Если лямбда-выражение не включает в себя лямбда-декларатор (lambda-declarator), оно интерпретируется так, как если бы лямбда-декларатор был пуст: (). Это позволяет опустить неиспользуемые () в простых лямбдах.
auto withParen = [s1 = std::move(s1)] () {};
auto noSean = [s2 = std::move(s2)] { /* Так можно */ };
Конкретно эти лямбды владеют строками, но не могут их изменить, s1 и s2 объявлены как const (т.к. лямбда по умолчанию const), для этого нам нужно ключевое слово mutable.
auto withParen = [s1 = std::move(s1)] () mutable { s1 += "d"; };
auto noSean = [s2 = std::move(s2)] mutable { /* Ошибка! */ s2 += "d"; };
Как ни странно, текущий стандарт требует наличия пустых скобок при применении mutable ключевого слова. Это правило контринтуитивно, приводит к распространенным синтаксическим ошибкам и загромождает код. При компиляции clang мы даже получаем синтаксическую ошибку, указывающую на то, что компилятор точно знает, что происходит:
example.cpp:11:54: error: lambda requires '()' before 'mutable'
Собственно, GCC 10 реагировал схожим образом, а уже GCC 11 стал заметно мягче относиться к нарушению стандарта:
warning: parameter declaration before lambda declaration specifiers only optional with '-std=c++23' or '-std=gnu++23' [-Wc++23-extensions]
А чего такого? - удивленно таращится на тебя компилятор, - в c++23 исправили же! Действительно, ребята добились своего, mutable оторвали от списка параметров. Однако в с++20 это все еще грех!

gcc lambda bug Конец апреля. Снег? Точно снег, я даже пожевал его, чтоб убедиться. Снег и некоторые органические вкрапления. Тихие всхлипы из подвального помещения взрезали тишину холодного белого утра. Это дворник Михалыч испытывал колоссальные нравственные страдания, вынужденный променять телегу на МАКС метелку обратно на снеговую лопату. Вдалеке послышался многоголосый леденящий душу вой - стали просыпаться оптимисты-автомобилисты. Наверное, я попал в прошлое, обратно в февраль. Это единственное объяснение. Если нет, то это какой-то страшный баг. Кстати, вся эта возня с багом 85282 навела меня на мысль, что внутриклассовая спецификация могла бы защитить некоторые методы в шаблонном классе от незаконной перегрузки и доступа через нелегально перегруженный метод к закрытым членам класса.
struct U {
  template <class T>
  void operator()() const {}
  template <> void U::operator()<int>() const {}
}
Теперь никто не сможет перехватить вызов operator()<int>() снаружи.
template <>
void U::operator()<int>() const {
  // доступ к закрытым членам класса
}
Это бы могло пригодиться для генерации лямбд, если каждый operator() инстанцировался внутри уникального класса, то перегрузить эти операторы не было бы никакой возможности. Хотя стоп, это и так запрещено: В разделе стандарта про лямбды 7.5.5.2 Closure types [expr.prim.lambda.closure] было указано в пункте 16:
Член замыкания не может быть явно инстанцирован, явно специализирован или указан во friend декларации.
Это значит, что мы не можем распоряжаться членом лямбды operator() как нам взбредет в голову. Взбрести может всякое. Не пытайтесь представить, эти картины сведут вас с ума. Лямбда - же это не просто ценный мех и синтаксический сахарок, это же некая особая сущность! Вот и стандарт это подтверждает. Правда, GCC? А GCC, отвернувшись, глядит куда-то вверх, будто он тут ни при чем:
auto x = []<class T>() -> void {
  // что-то делаем
};
Тогда я беру обычную лямбду уникального типа...
using X = std::decay_t<decltype(x)>;
Как тебе такое, GCC? Теперь мы знаем ее тип...
template<>
void X::operator()<int>() const { /* хаха, перехватили вызов */ }
Нет! GCC, да что ты творишь, почему ты компилируешь такое? Тебе вообще стандарт не указ? Вот clang так не дает делать:
error: lambda call operator should not be explicitly specialized or instantiated
И это чертовски правильно! Что же, не будем судить строго, однако это творожный сигнал. Похоже, это какие-то фундаментальные архитектурные проблемы не позволяют исправить баг двадцатилетней давности и напротив позволяют так грубо нарушать стандарт. Как обычно, все выводы на нашем канале вы делаете сами, но все всё понимают.

in-class explicit specializations. Bug 85282 Весна пришла, прилетели уточки с зимовки, отлетели кукухи... Будьте осторожны, в офисах фиксируются случаи нападения питонистов на людей! Ни в коем случае не подходите близко и не пытайтесь погладить или покормить с рук дикого разработчика. Выползший к людям обитатель опенспейса может быть переносчиком MAX или иной заразы. При встрече в узком коридоре, главное, не бегите, не смотрите в глаза и сохраняйте спокойствие. Медленно возьмите у него из рук телефон и разбейте о голову. Желательно не свою. Лишившись возможности писать в MAX рабочие чаты, психический, возможно, потеряет к вам интерес и вернется к работе. Но это не точно. Как писала Анна Каренина в MAX романе "Лев Толстой": "Все здоровые люди похожи друг на друга, каждый поехавший свистит флягой по-своему". Мне вот снится баг GCC 85282, который нужно обойти применительно к методам класса. Допустим, есть у нас operator(), который для всех типов выполняет одно действие, а для типа int - нечто совершенно иное. Сделать это вне класса очень просто, тут даже GCC не будет возражать:
struct U {
  template <class T>
  void operator()() const {
  }
}
Перехватываем оператор для типа int:
template <>
void U::operator()<int>() const {
}
Практика не очень чистая, но у стандарта вопросов не вызывает. Другое дело, если мы хотим это сделать внутри класса.
struct U {
  template <class T>
    void operator()() const {
  }
  template <>
  void operator()<int64_t>() const {
  }
}
А вот так не получится, стандарту это соответствует, а GCC - нет:
error: explicit specialization in non-namespace scope 'struct U'
Попробуем воспользоваться обходным путем из-под Палки. Если дополнить метод еще одним шаблонным параметром и попробовать специализировать частично, нас ждет разочарование:
template <class T, typename V = void>
void operator()() const {}
template <class T>
inline void operator()<T, void>() const {}
Ожидаемо получим только брань компилятора:
error: non-class, non-variable partial specialization 'operator()<T, void>' is not allowed
Эксперимент, очевидно, неудачный. Проведем другой, для счастливчиков, работающих с c++20
template <std::same_as<int*> T>
inline void operator()<T>() const {}
Компилятор опять будет вопить про недопустимость частичной специализации для методов. А если <T> убрать?
template <std::same_as<int*> T>
inline void operator()() const {}
Легким движением руки частичная специализация превращается... в элегантную полную. "Ну тогда можно, - скажет GCC и широко улыбнется, - тут просто перегрузка операторов: перекрытие общего более специализированным". Однако не стоит забывать, что у нас еще есть козыри в широких штанах: контракты!
template <class T>
inline void operator()() const requires (std::is_same_v<T, int>) {}
При этом не нужно менять основной operator(), т.к. компилятор считает контрактный оператор более специализированным и выбирает его. В общем, варианты есть. Баг 85282 не кажется таким уж страшным. Хотя не сигнализирует ли такой долгий ремонт бага об архитектурных проблемах в MAX с лямбдами?

in-class explicit specializations. GCC hotfix - Здравствуйте, моя фамилия Paleс, я разработчик из Чехии, часто пишу технические комментарии в темах про GCC, C++ и GDB. Возьмите меня компилятор разрабатывать? - Не-е-ет, ни за что! Нас засмеют же, - отвечают разработчики GCC, - следующий! - Приветствую! Я Patrick Palka... - О, другое дело! Заходи, такие люди нам нужны. Теперь никто не скажет, что наш компилятор пальцем деланный. Воистину, GCC не так прост. В 2022 году тот самый Patrick Palka прокомментировал ситуацию в треде бага 85282:
Ошибка не будет исправлена к моменту выхода GCC 12. Беда-беда, огорчение. Не знаю, насколько это будет полезно, но внутриклассовая явная специализация должна быть в большинстве случаев эквивалентна частичной специализации. Поэтому как обходной путь, могу предложить замену:
struct A {
  template<class T>
  struct B;

  template<>
  struct B<int> {}; // Ok, только GCC не компилирует
};
В C++20 мы можем исхитриться и написать:
struct A {
  template<class T>
  struct B;

  template<std::same_as<int> T>
  struct B<T> {};
};
Вроде как по всем признакам это частичная специализация, список шаблонных параметров непустой, но тип T ограничен только типом int. Вот так:
struct A {
  template<std::same_as<int> T>
  struct B {};
};
Была бы полная специализация, ограниченная типом int. С другим типом, вроде float, не соберется.
error: template constraint failure for 'template<class T>  requires  same_as<T, int> struct A::B
Если вам не повезло, и вы еще сидите на C++17, то есть другой путь:
struct A {
  template<class T, class = void>
  struct B;

  template<class T>
  struct B<T, std::enable_if_t<std::is_same_v<int, T>>> { };
};
Здесь мы добавляем в шаблон один лишний параметр, чтоб дальнейшая де-факто полная специализация выглядела бы как частичная. Мое почтение, господин Палка. Только вот для методов класса запрещена частичная специализация.

in-class explicit specializations Телега лежала уже третьи сутки. Иногда оттуда доносились сдавленные хрипы push-уведомлений, но кружащие сверху стервятники прозрачно намекали на вероятный исход. Не в силах на это смотреть, тоже прилег. Смысл что-то писать? Можно уже расслабиться или гиревым спортом заняться... Я с сомнением посмотрел на чугунную тушку с цифрой 32 на пузе. Нет! Не время для забав, напоследок все должны узнать, как GCC беспардонно попирает стандарт! Есть такая штука как CWG, Core Working Group. Рассматривает эта группа всякие интересные ситуации, вроде разнотыки в стандарте. Их, слава богу, хватает. Например, некто Faisal Vali еще в 2008 году заметил серьезный недочет во внутриклассовой явной специализации. Особо не мудрствуя, документ так и назвали CWG 727: In-class explicit specializations. Дело в том, что стандарт в пункте 13.9.4 [temp.expl.spec] втором параграфе требует, чтоб явные специализации шаблонных членов были в пространстве имен вне класса, а не внутри. Это ограничение не применяется для частичных специализаций шаблонных членов класса:
struct A {
  template<class T> struct B;
  template <class T> struct B<T*> { }; // Нормально
  template <> struct B<int*> { };      // Так нельзя
};
Непоследовательно это как-то беспричинно-несправедливо. Действительно, сказали разработчики компиляторов и примерно лет через десять починили это. Во-первых, на уровне стандарта. В стихе втором раздела 14.7.3 Explicit specialization [temp.expl.spec] раньше было:
Явная специализация должна быть объявлена в пространстве имен, где обитает основной шаблон, либо во внешнем по отношению к нему. Явная специализация по неквалифицированному имени, т.е. без перечислений пространств имен, должна быть объявлена в ближайшем пространстве имен базового шаблона, либо в любом его inline пространстве.
А если посмотреть свежие версии стандарта, то уже в третьем параграфе обнаруживается существенное послабление.
Явная специализация может быть объявлена в любой области, где объявляется базовый шаблон.
Это относится и к методам класса:
struct A {
  temaplate <class T>
  void meth() {}

  template <>
  void meth<int>() const {}
};
Во-вторых, разработчики подправили компиляторы. Однако не все, у GCC, как всегда, есть особое мнение на этот счет. Закономерным итогом строптивости стал заведенный в 2018 году неким songyuanyao баг 85282, что напрямую связан с исправлением cwg727. Изложены там очевидные вещи, что не выполнены постановления ЦК КПСС по смягчению правил явной специализации. Багу этому много лет, и он еще не решен. GCC до сих пор нарушает стандарт и запрещает нам явную специализацию внутри класса. Это может дать очень интересный эффект в самый неожиданный момент.

Хромая телега - Так понимаю, пиццы совсем не будет? Картонная дверь офисной кухни захлопнулась, отвратительно клацнув дешевым замком. Меня обступили грозно насупившиеся разработчики. Навязчивое предложение угоститься ананасной пиццей в честь второго подряд зеленого билда оказалась ловушкой. Стоило догадаться. Слишком хорошо, чтоб быть правдой. Внезапно человеческая масса пришла в движение. Послышалась тяжелая поступь руководитель проекта по прозвищу Пиэм. Он прошел через толпу, словно атомный ледокол сквозь стайку надувных матрасов, и встал напротив. Грузное тело угрожающе нависало надо мной, как дедлайн над разработчиком. Грозное лицо застыло, превратившись в маску Го Сяна, что не предвещало ничего хорошего. - Сядь, - тихо попросил он, - и телефон покажи товарищам. - А что тебе мой телефон? - огрызнулся я, но на всякий случай присел. - Да ходят слухи, что ты национальный мессенджер установил... "Спалили!", - холодея от ужаса подумал я и опустил глаза. Пиэм, видя такое красноречивое саморазоблачение, начал преисполняться в своем негодовании. - ... в такие дни, как наши, когда каждый из нас должен отдать все свои силы на противодействие принуждению репрессивного аппарата к неудобным цифровым решениям, в такие дни особенно важно для нас всех сплотиться и не допускать в наши ряды небезопасные информационные взаимодействия, необходимо агитировать, повышать морально-нравственный уровень каждого, продвигать духовную чистоту, цифровую гигиену... - И животноводство! - вскричал вдруг требовательно питонист Скоробогатов. - Безусловно... это бесспорно... - замялся Пиэм, - и животноводство тоже... Стыд ел мне глаза. - Товарищи! - возопил я, - Так ведь все уже там! Даже Саша Конь! - Ты не Конь! - взревела толпа паровозным гудком. - Да ведь у меня дети, школа там, все дела... меня заставили... Пиэм громко высморкался, толпа смолкла. - Товарищи все понимают, - наконец сказал он, - жди экспоуз в телеге. "Звучит страшно, - подумал я, начиная волноваться, - а что бы было, если бы они узнали про мой канал там? Отменили бы, как есть отменили!". Правда, канальчик-то приватный, а значит, его не найдут, если не перейдут по прямой, хоть и некрасивой, ссылке: https://max.ru/join/mavpKe0WRs0bt6q97pdB65dcg2N-Q6L0BTILZ_1otFA Мало ли, не у всех есть время с блокировками бодаться. Кого-то, может быть, работой завалило как Камчатку снегом, а ему срочно приспичило разоблачить, как GCC который год попирает стандарт, и ведь никому никакого дела!