Крестовый подход
834 subscribers
2 photos
15 links
C++ | Актуальные новости и авторские статьи от эксперта индустрии

https://delimbetov.dev - консультации и тренинги
@krestovii_podhod_chat - для обратной связи
Download Telegram
#новости
Герб Саттер рассуждает о эволюции C++ в области безопасности на фоне заявлений Белого Дома о memory safe языках.

Кратко
На плюсах слишком просто писать небезопасный код. 4 основные категории ошибок в работе с памятью: lifetime, bounds, type, initialization. От 98% багов из этих категорий можно защититься в новом коде не ломая обратную совместимость. По умолчанию компиляторы и экосистема в целом не настроены на максимизацию безопасности.

Мнение
Мне нравится идея, что все настройки по умолчанию должны быть выкручены в сторону безопасности. Огромный пласт плюсового кода не про производительность, как считают некоторые, а про обратную совместимость и кроссплатформенность. Для такого кода подобные проверки не приведут к потере производительности, но помогут исправить потенциально критичные баги.

Читать надо держа в уме недавние заявления из Вашингтона. Напомню, Белый Дом решил выступить в поддержку memory safe языков программирования. Это произошло не просто так, а потому что баги связанные с памятью легко превращаются в эксплоиты, соответственно уменьшение количества таких багов в ключевой инфраструктуре страны и ее главных компаний вопрос нац. безопасности.

Первой реакцией C++ комьюнити было наштамповать мемов о том, что Байден топит за раст. Но на самом деле это очень тревожный звоночек для нас (плюсовиков). Например, правительство США может запретить использование C++ и других не memory safe языков в госзаказе, а это огромная часть индустрии.

Очень хорошо, что один из самых влиятельных людей в мире плюсов отнесся к поднятой теме серьезно и начал предлагать варианты исправления существующих проблем. Эта статья только наброс для поощрения дальнейшего обсуждения, но я считаю это отличным стартом продуктивной дискуссии.
#новости
На этой неделе в 🇯🇵 Токио была очередная встреча комитета по стандартизации C++, и начали выходить отчеты (раз, два) от непосредственных участников.

Главное

Рефлексия - P2996 - рабочая группа проделала огромную работу, есть экспериментальная имплементация как минимум в двух компиляторах (в форках, конечно же). Фидбек очень позитивный, хорошие шансы попасть в C++26.

Контракты - P2900 - консенсуса пока нет. Ребята из Microsoft недовольны текущим пропозалом, ведь там при проверке контракта разрешается UB, по их мнению это нарушение принципа safety-by-default на которое SG21 (рабочая группа по контрактам) согласилась. Пока непонятно, успеют ли разрешить конфликты к C++26.

Так же одобрили много мелких фич (полный список в отчетах), из которых мне кажется наиболее интересным P2795 - меняется то, как компилятор трактует неинициализированные обьекты. Вводится новое понятие "erroneous behaviour" (не путать с UB!), которое является формальным термином для описания проблемного (с точки зрения логики) кода, но не являющегося риском для безопасности. Теперь неинициализированные инты будут нулями. Что не значит, конечно, что можно писать такой код, ваш тулинг должен отлавливать неинициализированные переменные.
#мнение
Наконец-то удалось поближе познакомиться с внутренним устройством корутин из C++20. Попал на тренинг от Mateusz Pusz по этой теме. Хочу поделиться впечатлениями и мыслями.

Корутины
В теории плюсовые корутины не сильно отличаются от, например, питоновских. Функции “красятся” возвращаемым типом почти так же, как async в питоне. Если функция покрашена, в ней можно co_await-ить другие корутины. Все кажется просто - но, к сожалению, это не так.

Поскольку C++20 стандартизировал только механизм корутин, в библиотеке нету аналога модуля asyncio. А значит нету единого интерфейса ранлупа, который используют библиотеки, предоставляющие корутины в интерфейсе. Каким образом добиться сетевого эффекта распространения при таком ограничении мне непонятно. Скорее всего, на это и не рассчитывали, задача была предоставить низкоуровневые примитивы и отдать остальное на откуп сообществу. В реальности, при желании использовать нативные корутины вам скорее всего придется разбираться в их устройстве, а там все сложно даже по плюсовым стандартам.

Детали написания своей awaitable task, это, наверное, самое сложное с чем я когда-либо разбирался в языке. Комбинация большого числа абстракций, неудачного нейминга, а так же крайне неочевидного для непосвященных смысла в низкоуровневых нюансах делает опыт работы с, казалось бы, теоретически не сложным концептом, сущим адом.

Субъективно кажется, что в текущем виде никакой надежды на массовое распространение нет (учитывая что сейчас уже 2024, можно быть в этом уверенным). Единственный реалистичный юзкейз, это все та же привязка кодовой базы к одному фреймворку, который будет эти детали прятать под капотом. Тогда получается что единственное отличие - наличие синтаксического сахара co_await.
#база
Привет! Написал статью о указателях (сырых и умных) C++.

Так же на прошлой неделе прошла конференция ACCU в Бристоле. Было очень много интересного, выложу свои рекомендации по лекциям, как только они появятся на ютубе.
#интересное
Можно ли реализовать type erasure (стирание типа) без виртуальных вызовов?

На реддите - кстати, рекомендую /r/cpp, там довольно высокий уровень контента и в комментарии иногда заходят люди уровня Эрика Ниблера (автор P2300 и P0896) - кто-то запостил std::function, реализованную без наследования. Мне стало интересно разобраться в вопросе: как работает, какие варианты существуют.

Стирание типа
Базовая идея идиомы - оборачивание не связанных типов с одинаковым интерфейсом в какой-то общий тип. Классический пример - как раз std::function, в которую можно положить любой тип, имеющий оператор вызова (лямбда, указатель на функцию, функтор).

void c_func() {}

struct Functor {
void operator()() {}
};

auto func = std::function<void()>();

func = c_func; // OK: обычная функция
func = Functor(); // OK: функтор
func = [] {}; // ОК: лямбда

func(); // вызов последнего записанного в переменную объекта


Виртуальные вызовы
Самый простой, и, наверное, известный способ реализовать подобный интерфейс - через скрытое наследование. Идея в том, что ваш тип-обертка спрячет переданный вызываемый объект в шаблонный тип, типизированный типом этого объекта. Шаблонный тип же наследует от базового интерфейса с виртуальный функцией вызова.

Базовый пример (без поддержки разных сигнатур функций):
struct Function {
template <typename T>
Function(T&& func)
: _impl(
new Concrete<std::decay_t<T>>(
std::forward<T>(func))) {}

void operator()() {
_impl->call();
}

private:
struct Impl {
virtual void call() = 0;
};

template<typename T>
struct Concrete : Impl {
Concrete(T&& func)
: _func(std::move(func)) {}

void call() final {
_func();
}

T _func;
};

Impl* _impl;
};


Void*
Альтернативный способ, как раз предложенный в ссылке в начале поста, заключается в использовании указателей на функцию. В примере выше мы, в какой-то степени, делаем то же самое. Разница становится видна при попытке поддержать передачу функциональных объектов с состоянием: лямбда с захватом переменных, функтор с данными в теле объекта. Такие объекты нельзя привести к указателю на функцию. Состояние объекта не является частью типа, поэтому приходится хранить его отдельно.

Упрощенная версия:
struct Function {
template <typename T>
Function(T&& func) {
using TD = std::decay_t<T>;
_func = [](void* state) {
auto* casted = (TD*)state;
(*casted)();
};
_state = new TD(std::forward<T>(func));
}

void operator()() {
_func(_state);
}

private:
void (*_func)(void*);
void* _state;
};

(тут есть утечка, но обработка требует много кода; смотрите по ссылке в начале поста полноценную реализацию)

Есть ли разница
С точки зрения читаемости и безопасности первый вариант однозначно лучше. В реальном коде, по моему мнению, он должен быть дефолтным выбором. Однако у второго может отличаться профиль производительности за счет использования немного другого механизма вызова переданной функции, так что теоретически он может быть производительнее (но, конечно, надо бенчить если производительность имеет значение).
#база
Сегодня у меня для вас разбор базовых стратегий обработки ошибок в плюсах: плюсы и минусы исключений и сишного стиля.

std::expected решил не упоминать, но для интересующихся советую знаменитую лекцию (англ.) Андрея Александреску - смотрится крайне легко, потому что разбавлено юмором в удачной пропорции.
#будущее
Почему в 24 году нам приходится писать каст енума к строке вручную?

Сегодня рассмотрим пропозал P2996 (рефлексия) и разберемся как он нам поможет с этой проблемой, и как еще сделает нашу жизнь легче. Я опишу основные моменты, а так же самые интересные примеры использования.

Рефлексия это возможность программы исследовать или изменять саму себя. Можно разделить на 2 вида - динамическая и статическая. Динамическая рефлексия доступна в рантайме (во время выполнения программы, а не во время компиляции). Статическая рефлексия работает во время компиляции.

P2996 предлагает только статическую рефлексию. Вводятся два новых оператора и новый хидер <meta> с набором полезных функций.

Изменения языка
1. Новый оператор ^ - да, это переиспользование xor - производит reflection value (reflection/отражение) из типа, переменной, функции и тд. Отражение имеет тип std::meta::info и по сути является ручкой для доступа к внутреннему строению отражаемого типа.
2. Splicers - [:R:] - где вместо R вставляется ранее отраженное значение. Переводит std::meta::info обратно в обычный тип/переменную/функцию.

Изменения библиотеки
1. Новый тип std::meta::info - для представления отражения.
2. Метафункции в <meta>, например: members_of - получить список членов какого-то класса, enumerators_of - список констант в переданном енуме, offset_of - отступ переданного субъобъекта (учитывает паддинг), is_noexcept - является ли переданная функция noexcept и многое другое.

Использовать все это совсем не сложно, особенно если вы ранее работали с шаблонами.

Примеры (взяты из пропозала)
Получение отражения и возврат к изначальному типу
// отражение
constexpr auto r = ^int;
// int x = 42;
typename[:r:] x = 42;
// char c = '*';
typename[:^char:] c = '*';


Обращение к члену класса по имени
class S { int i; int j; };

consteval auto member_named(std::string_view name) {
for (std::meta::info field : nonstatic_data_members_of(^S)) {
if (name_of(field) == name)
return field;
}
}

S s{0, 0};
// s.j = 42;
s.[:member_named("j"):] = 42;
// Ошибка: x не часть класса.
s.[:member_named("x"):] = 0;


Функция member_named принимает имя члена класса. С помощью std::meta::nonstatic_data_members_of запрашивается список имеющихся членов класса, для каждого элемента списка запрашивается его имя с помощью std::meta::name_of. Тот член у которого совпадет имя с переданным в функцию и будет использован.

Шаблонная функция перевода енума к строке
template <typename E>
constexpr std::string enum_to_string(E value) {
template for (
constexpr auto e : std::meta::enumerators_of(^E)) {
if (value == [:e:]) {
return std::string(
std::meta::name_of(e));
}
}

return "<unnamed>";
}

enum Color { red, green, blue };
static_assert(enum_to_string(
Color::red) == "red");


Функция принимает константу из произвольного енума, отражает его тип, с помощью std::meta::enumerators_of получает список констант этого енума и матчит его с переданной константой. Найденное отражение передается в std::meta::name_of, который возвращает имя константы из обьявления этого енума.

Заключение
В статье (см ссылку вначале поста) есть куча других примеров, советую хотя бы пробежаться взглядом. Самый интересный, субъективно, это универсальный форматтер. С помощью него можно будет написать шаблонную функцию, которая сможет переводить любой класс в строку без дополнительного кода. Представьте сколько миллионов строчек кода в мире станут ненужными только за счет этого!

Пропозал еще не принят комитетом, так что теоретически в C++26 мы можем его не увидеть. Однако наличие рабочих имплементаций, зрелость пропозала и поддержка сообщества позволяют надеятся, что в следующий стандарт рефлексия попадет.
#будущее
Еще немного про рефлексию

В предыдущем посте речь шла про основной пропозал - P2996. Однако есть и дополнения к нему со своими интересными фичами. Сегодня рассмотрим P1306 и P3096.

Expansion statements - P1306
Представьте, что у вас есть некоторая коллекция объектов разных типов (например, tuple) и вы хотите по ней проитерироваться. Обычный range loop этого не умеет, потому что переменная, с помощью которой происходит итерация, может быть только одного типа. Есть ухищрения вроде std::apply и переводом коллекции в template pack, но это требует дополнительного кода и субъективно довольно костыльно.

Пропозал предлагает новый оператор - template for - он упоминается и в P2996, поскольку этот функционал значительно упрощает написание многих мета функции.

Базовый пример из пропозала:
auto tup = std::make_tuple(0, ‘a’, 3.14);

template for (auto elem : tup)
std::cout << elem << std::endl;


Это один из редких случаев в плюсах, когда интуитивно понятно что делает новая фича. Под капотом все тоже в целом не сложно, но все таки надо упомянуть, что компилятор разворачивает данный цикл примерно в следующее:

{
auto elem = std::get<0>(tup);
std::cout << elem << std::endl;
}
{
auto elem = std::get<1>(tup);
std::cout << elem << std::endl;
}
{
auto elem = std::get<2>(tup);
std::cout << elem << std::endl;
}


template for это не цикл в классическом его понимании, а способ продублировать блок кода для каждого элемента в коллекции, что позволяет элементам быть разного типа.

Рефлексия параметров функции - P3096
Фича позволяет получить доступ к информации об аргументах функции. Пример из пропозала, который показывает, как можно написать вывести все аргументы функции, явно их не перечисляя:
void func(
int counter, float factor) {
template for (
constexpr auto e
: parameters_of(^func))
cout
<< name_of(e) << ": "
<< [:e:] << "\n";
}


В предлагаемую пропозалом мета функцию std::meta::parameters_of передается текущая функция. parameters_of возвращает вектор с отражениями аргументов функции. std::meta::name_of извлекает имя аргумента из отражения, а [:e:] извлекает значение аргумента в текущем вызове функции. Кстати, этот функционал уже доступен на годболте.

P3096 довольно спорный пропозал - возможно именно поэтому он предлагается отдельно от P2996. Дело в том, что стандарт позволяет объявлять одну и ту же функцию сколько угодно раз, и с какими угодно именами аргументов. Например:
// file1.h
void func(int value);

// file2.h
void func(int not_a_value);

// file3.cpp
constexpr auto names = meta::parameters_of(^func); // ?


Вопрос, какое имя интового аргумента должна вернуть parameters_of: value или not_a_value? В пропозале представлена аргументация в пользу разных решений, но предлагается следующее: при вызове parameters_of компилятор будет проверять консистентность именования, и если есть несовпадения, то это ошибка компиляции. Таким образом существующий код не ломается, хотя и немного ограничивается область применения новой мета функции.

Примут ли это в стандарт?
Пока точно сказать нельзя. Авторы надеются, что все попадет в C++26 вместе с основным пропозалом, я бы тоже этого хотел, так как функционал крайне полезный. Будем следить за следующими собраниями стандартного коммитета, ближайший уже скоро - 24 июня в Сент-Луисе.
#новости
На прошлой неделе в Сент-Луисе 🇺🇸 проходила очередная встреча группы WG21, так же известной как стандартный комитет C++. А на этой начали появляться трип репорты от непосредственных участников (Герб).

Главное
P2996 - фаворит народа, ожидаемая многими рефлексия - получила design approval в части изменения языка (операторы), но еще находится на рассмотрении в части дизайна библиотеки (<meta>). Это еще не гарантия попадания в C++26, но вероятность близка к 100%, учитывая что до дедлайна принятия в стандарт еще целых 11 месяцев. Ура!

P3000 - std::execution, так же известная как Senders/Receivers и экзекьюторы - принята в C++26. По сути это фреймворк для решения проблемы многозадачности и совместного использования вычислительных ресурсов различными не связанными библиотеками. Лично у меня смешанные чувства, потому что есть сильное ощущение что эту фичу основная масса разработчиков использовать не будет - слишком сложно. С другой стороны, это просто библиотека, поэтому при отсутствии желания никто не заставляет с ней взаимодействовать. Кстати, скоро будет пост с разбором этой фичи!

P2900 - контракты - Герб пишет что прогресс есть, но все еще остаются спорные вопросы. Шансы попасть в стандарт, по его оценке, 50/50.

А еще
P0843 - std::inplace_vector - принят. Интересный контейнер, реализации которого в том или ином виде существовали уже лет 20, наверно, если не больше. Интерфейс вектора, но с заданным во время компиляции максимальным размером (capacity). То есть ресайзить до любого размера <= capacity можно. Бонус в том, что данные находятся на стеке и не надо аллоцировать. Отличие от std::array в том, что дополнительно хранится "логический" размер контейнера.

Следующее собрание комитета пройдет 18-23 ноября в Вроцлаве🇵🇱.
#интересное
Безопасно ли писать из разных потоков в две переменные char, лежащие рядом в памяти?

О чем, собственно, речь?
Среднестатистическому программисту, не занятому в супер нишевых областях вроде HFT, не нужно для работы разбираться в низкоуровневых вещах, вроде оптимизации использования CPU кэша и его архитектуре. Однако бОльшая часть из моих бывших и текущих коллег интересуются тем, как писать производительный код - а иначе можно ли называть себя плюсовиком? : )

Интерес к низкоуровневым оптимизациям присутствует в комьюнити - и это, в том числе, проявляется в виде отрывочных знаний, которые присутствуют в сознании значительного подмножества программистов. К сожалению, некоторые знания могут быть неправильными и даже вредными, но большинство вполне имеют место быть.

Пара примеров
На интервью на плюсовые вакансии уже давно (года так с 17, если не раньше) стало модно спрашивать что-нибудь поверхностное про кэш процессора - лично мне особенно часто встречался вопрос вида “назовите отличия std::list и std::vector”, где часть ответа обязательно должна быть про то, что вектор хранит данные в последовательном куске памяти, что позволяет процессору быстрее получать к ним доступ (за счет лучшей утилизации кэш линий, возможности больше префетчить, и тд).

Возможно вы слышали, что любой доступ к переменной встроенного типа (int, raw pointer, char, etc.), если размер этого типа <= размеру регистра процессора, атомарен, поскольку выполняется одной инструкций. Это, насколько я знаю, действительно так. Однако многие делают из этого вывод, что использовать std::atomic при конкурентном доступе к переменной такого типа необязательно. Это пример заблуждения: компилятор может собрать код так, что будет работать и без std::atomic, но согласно стандарту это - дата рейс, а значит UB. И даже если UB не приведет к крашу, гарантий что логика будет корректно отрабатывать нет.

Эти и подобные знания, не всегда подкрепленные сильной теоретической базой и/или глубокой экспертизой в вопросе, могут привести к недопониманию там, где, кажется, все должно быть очевидно. По крайней мере в моем случае : )

Так что там с 2 char
static_assert(sizeof(char) == 1);
alignas(64) char array[2] = {0};

// thread 1
array[0] = 1;

// thread 2
array[1] = 2


Есть ли тут data race?
Эксперты плюсового стандарта сразу скажут, что нет - это одна из гарантий memory model, определенной в C++11. Простыми словами: взаимодействие с разными объектами (переменными, или элементами массива) всегда безопасно, даже если они находятся рядом в памяти.

Но не очевидно, как это реализовано на низком уровне. Кажется, что это исключает возможность некоторых оптимизаций компилятором - возможно поэтому в супер оптимизированном коде, по крайней мере из того что я видел, для вычислений используют типы size_t (по размеру регистра).

Аргумент за data race - array находится в одном 2 байтовом блоке. Если регистры 32/64 битные, то как может запись одного байта происходить без записи в 3/7 соседних байт? А если запись идет сразу в 4/8 байт, то как происходит меж-ядерная синхронизация, ведь записывая в array[0], мы перезаписываем данные в array[1] тоже? Понятно, что можно маской обновлять только 1 байт из 4/8, но запись то происходит во все 4/8!

На сколько я смог разобраться: несмотря на то, что регистры 32/64 битные, у них присутствует возможность адресации первых 8/16 бит. Таким образом есть возможность пометить “грязными” только конкретно эти 8/16 бит. Однако суть даже не в этом. Обе переменные находятся на одной кэш линии, а любая запись в кэш линии инвалидирует ее для других ядер. Иначе говоря, об этом можно думать как о получении одним из ядер эксклюзивного (как у мьютекса) владения кэш линией на время записи. Алгоритм когерентности кэша называется MESI. Это же поведение является причиной достаточно известного явления - false sharing, когда происходит пинг-понг инвалидации одной и той же кэш линии двумя ядрами с печальными последствиями для производительности.
#будущее
Чего не хватает в языке, изобретенном 40 лет назад, и активно разрабатываемом с тех пор?

Например, возможности написать int x; и не получить UB. Недавно вышла статья (англ.) на эту тему . Советую ознакомиться - читается легко, и довольно интересно. Или можете прочитать мою краткую выдержку : )

В чем проблема
struct S {};

S s; // default constructor
int x; // bad state


Переменная s, будучи объектом класса S, проинициализирована конструктором по умолчанию, никаких проблем нет.

Переменная x, будучи встроенным типом int, не проинициализирована вовсе. На практике это значит следующее:

1. В области памяти, которую занимает x, находится мусор. Если вы забудете присвоить какое-либо значение позже, то этот мусор будет участвовать в вычислениях. Это довольно опасно - чтение из “мусорной” переменной это не просто баг, ломающий логику программы, но и дыра в безопасности. И это не теоретическая проблема, а вполне реально используемый способ взлома. См примеры атак тут;
2. Дальнейшее использование x считается UB.

Что если присвоить значение потом?
int x;

// do something

x = 0;


В текущем стандарте даже присвоение значения после создания непроинициализированного инта не является инициализацией. То есть использование x остается UB даже после присвоения значения. На практике, конечно, такой код слишком часто встречается, чтобы компиляторы могли себе позволить собрать из него что-то опасное, но чисто формально у них есть на это право : )

Скоро будет лучше
В июне был принят пропозал P2795, который будет реализован в C++26.

Ключевые изменения
1. Чтение из непроинициализированного инта более не UB. Это все равно логический баг вашей программы, но более не дыра в безопасности. Реализовано это будет путем записи какого-то значения (не обязательно нуля) в переменную по умолчанию.
2. Да, это значительно повлияет на производительность некоторых программ, поэтому можно будет явно отключить новое поведение там, где это необходимо. Для этого добавляется новый атрибут [[indeterminate]]

int x; // erroneous state

cout << x; // более не UB!


int x; // erroneous state

x = 1;

cout << x; // все ОК


int x [[indeterminate]];

cout << x; // все еще UB : )


Кажется, что в последнее время комьюнити наконец начало обращать внимание на проблемы с безопасностью плюсов. По моему мнению, постепенный переход к парадигме safe-by-default вместо performant-by-default это более выигрышная стратегия для будущего языка. Отсутствие UB и логических ошибок полезно в любой программе, а экстремальная производительность только в очень маленьком подмножестве.
#новости
Продолжение истории о memory safety в C++

Контекст
В начале этого года вопросы безопасности в C++ вышли на новый уровень — ими заинтересовалось даже правительство США. В феврале они выпустили призыв использовать языки программирования, спроектированные с учётом безопасности работы с памятью. Это подкреплялось данными, что до 70% уязвимостей и эксплойтов связаны с ошибками работы с памятью.

Эта ситуация стала уже не просто предупреждением, а настоящим сигналом тревоги: если мы хотим, чтобы C++ продолжал оставаться одним из ведущих языков разработки, нужно что-то менять.

Одной из громких реакций была мартовская статья Герба Саттера, с которой он потом объехал множество конференций. Конечно и до вмешательства Вашингтона в сообществе шли активные обсуждения memory safety. Проекты вроде Carbon и cpp2 уже пытались предложить решения, но пока безуспешно.

Новость
11 сентября Шон Бакстер, автор компилятора Circle, представил своё видение: пропозал P3390, или Safe C++. Очень рекомендую ознакомиться с разделом “1.5 Categories of Safety” — там приведены довольно наглядные примеры.

Если кратко, то предлагается явно отделять безопасный код от небезопасного (safe keyword). Добавляется borrow checker, destructive move (relocation), безопасная версия STL и многое другое.

Да, это все очень похоже на Rust : )

Мнение
Я полностью поддерживаю повышенное внимание к проблемам memory safety — их придётся решить так или иначе. Однако есть большие сомнения, что предложенные изменения пройдут через стандартный комитет. А если это и произойдёт, то, вероятно, не раньше, чем через лет 10. На данный момент, куда более прагматичными кажутся решения, которые не требуют нового синтаксиса и могут быть сразу применены ко всей кодовой базе. Например, использование флагов компиляции для автоматической вставки проверки границ в operator[].
#новости
Стратегия гугла по memory safety и новое препятствие рефлексии в C++26

1. Гугл и memory safety
Инженеры гугла делятся планами по уменьшению количества багов при работе с памятью .

Кратко
> Новый код стараются писать на безопасных языках. Rust стали использовать за рамками Андроида. Продолжают работать над Carbon (безопасный язык со встроенной совместимостью с плюсами).
> Проверенный и стабильный C++ код не переписывают, но пересобирают с дополнительными рантайм проверками (out of bounds). Продолжают инвестировать в фаззинг (тесты с рандомными входными данными)

2. Рефлексия может задержаться

Вышел новый пропозал core рефлексии - P3435. Это не продолжение P2996, уже частично прошедшего одобрение комитета, а его замена. Если у авторов получиться объяснить комитету, почему их пропозал стоит серьезного рассмотрения, то уже находящийся на финишной прямой P2996 может быть отложен на неопределенный срок.

P3435 позаимствовал часть P2996, но есть ключевые отличия:

1. Вместо std::meta::info, представляющего все виды отражений, вводятся 3 разновидности: decl, expr, type.
2. Мета функции используют новые built-in типы-контейнеры вместо std::vector в своем интерфейсе.
3. Предлагается возможность отражать и конструировать выражения.