- PVSM.RU - https://www.pvsm.ru -

ISO C++ — встреча международного комитета в Польше

В конце ноября состоялась встреча международного комитета по стандартизации языка программирования C++.

ISO C++ — встреча международного комитета в Польше - 1

В этот раз без внимания не остались темы:

  • Рефлексия времени компиляции и оператор «монобровь»
  • Constexpr, много constexpr
  • SIMD
  • Structured bindings as a pack
  • Безопасность, контракты, libc++ hardening, профили, UB и std::launder
  • Сколько бит в байте?

Рефлексия времени компиляции

Reflection, а именно предложение P2996 [1], на полном ходу движется к принятию в C++26.

Сейчас есть как минимум три имплементации. Одна из них открытая [2]. Некоторые из имплементаций доступны на https://godbolt.org/ (вот только там подустаревшие прототипы, так что современные примеры пока не собираются).

Интересный момент: текущее предложение по рефлексии не позволяет работать с атрибутами. Об этом сожалели многие разработчики (в том числе и на Хабре), так как это мешает навешивать произвольные данные на типы и поля… UPD: Не смотря на то, что рефлексия позволяет работать с алиасами, получить их из типа не получится:

template <class T, auto name>
using JsonField = T;

JsonField<std::string, "user-name"> name{};

static_assert(  // увы, не сработает :(
    // `type_of` отбрасывает информацию об алиасе
    [: template_arguments_of(type_of(^^name))[1] :]
    == std::string_view{"user-name"});

// Убираем алиас-информацию
static_assert(dealias(type_of(^^name)) == ^^std::string);

И да, было решено превратить оператор для рефлексии ^ в ^^ (без пробела между символами). С лёгкой руки некоторых участников оператор назвали unibrow — «монобровь».

P2996 [1] всю неделю варился в подгруппах LWG и CWG и там наблюдался весьма неплохой прогресс, но в черновик стандарта его пока ещё не вмерджили. При этом многие вещи, необходимые для рефлексии, были вынесены в отдельные constexpr-предложения — и их-то как раз приняли.

constexpr

Итак, теперь в compile time можно выкидывать и ловить исключения! Всё благодаря предложению P3068 [3]. Оно, как ни странно, также улучшает и безопасность программ, потому что теперь следующий код провалится на этапе компиляции, вместо того чтобы выкинуть исключение на рантайме при использовании функции foo:

constexpr int foo(int x) {
    // Невалидная дата передана в constexpr-конструктор. Конструктор теперь 
    // отработает на этапе компиляции, будет выброшено исключение,
    // которое никто не перехватывает... Компиляция прервётся.
    const Date date{"1900-23-01"};  
    return date.days() + x;
}

Атомарные типы atomic<T> и atomic_ref<T> тоже стали constexpr в P3309 [4]. Теперь, например, можно писать многопоточный generic-код, и выполнять его в один поток во время компиляции:

constexpr bool process_first_unprocessed(std::atomic<size_t> & counter,
                                         std::span<cell> subject)
{
	const size_t current = counter.fetch_add(1); 
	
	if (current >= subject.size()) {
		return false;
	}
	
	process(subject[current]);
	return true;
}

constexpr void process_all(std::span<cell> subject, unsigned thread_count = 1) {
	// ОК выполнить в compile time, если thread_count == 1
	std::atomic<size_t> counter{0};
	auto threads = std::vector<std::jthread>{};
	
	assert(thread_count >= 1);
	
	for (unsigned i = 1; i < thread_count; ++i) {
		threads.emplace_back([&]{
			while (process_first_unprocessed(counter, subject));
		});
	}
	
	while (process_first_unprocessed(counter, subject));
}

Structured bindings также стало возможно хранить как constexpr-переменные благодаря P2686 [5]. Теперь constexpr static auto [v0, v1] = A{2, 3}; скомпилируется.

Алгоритмы тоже не остались без внимания: в P3508 [6] и P3369 [7] обросли constexpr алгоритмы std::uninitialized_* и std::ranges::uninitialized_*. Теперь писать свои constexpr-алгоритмы стало ещё проще!

SIMD

В черновик стандарта C++26 приняли SIMD P1928 [8]. Другими словами, появилась возможность векторизировать обработку данных в платформонезависимом виде:

void sinuses(std::span<float> data) {
    using floatv = std::simd<float>;
    auto it = data.begin();
    for (; it <= data.end() - floatv::size(); it += floatv::size()) {
        // Прочитает сразу floatv::size() чисел, допустим 8
        floatv vec(it);

        // Сразу для 8 чисел посчитает синус и запишет результат обратно в data
        std::sin(vec).copy_to(it);
    }
    for (; it < data.end(); ++it) {
        *it = std::sin(*it);
    }
}

В зависимости от того, под какой процессор скомпилировано ваше приложение, результат floatv::size() будет отличаться. Если архитектура поддерживает эффективную работу с 8 float одновременно, то будет 8. Если соберёте приложение под совершенно другую архитектуру (даже не x86), то floatv::size() выдаст значение именно под эту архитектуру (*).

* Если вы разрабатываетесь под архитектуру RISC-V, возможны нюансы. Пожалуйста, посмотрите насколько std::simd хорошо ложится на вашу архитектуру, и напишите свои мысли в комментах (или мне в личку).

При работе с simd не стоит забывать про обработку последних значений, как показано в примере выше. У автора предложения была идея упростить работу с последними значениями, но пока что подобное предложение не попало в стандарт.

void sinuses(std::span<float> data) {
    std::for_each(std::execution::simd, data.begin(), data.end(), [](auto& v) {
        v = std::sin(v);
    });
}

Ах да, std::simd тоже помечен как constexpr и им можно пользоваться в compile time.

Structured bindings as a pack

Хорошая новость! В C++26 можно будет не пользоваться Boost.PFR для поиндексного обращения к элементам, так как эта функциональность библиотеки будет доступна прямо в ядре языка C++. Благодаря P1061 [9] можно раскладывать кортежи и агрегаты произвольных размеров на отдельные поля, а вместе с принятым ранее P2662 [10] удобно к ним обращаться:

[](const auto& some_struct) {
    auto [...x] = some_struct;
    static_assert(sizeof...(x) < 3, "Too many fields");
    if constexpr (sizeof...(x) == 2) {
        foo(x...[1], x...[0]);
    } else {
        foo(x...);
    }
}

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

Прочие нововведения C++26 с последней встречи

Добавились типы std::indirect<T> и std::polymorphic<T> P3019 [11]. Первый предназначен для хранения объекта по указателю. При этом его копирование приводит к копированию объекта, на который он ссылается. Полезная штука для создания рекурсивных структур и вынесения больших подобъектов в динамическую память, чтобы сам объект мог умещаться на стеке. std::polymorphic<T> работает аналогично, но позволяет хранить в себе наследников от T, обеспечивая их правильное копирование.

Предложение P3138 [12] добавило std::views::cache_latest, для кеширования подсчитанных значений. Это позволяет для transform(f) | filter(g) кешировать результат трансформации, и не звать f дважды для каждого успешно прошедшего фильтрацию значения.

Ещё приземлилось множество оптимизаций для линейной алгебры и работы с многомерными спанами в P2897 [13], P3222 [14], P3050 [15], P3355 [16].

А также как всегда множество небольших багфиксов описаний в черновике стандарта — как для стандартной библиотеки, так и для ядра языка.

Безопасность

Безопасность — очень широкий термин. Многие в него вкладывают разный смысл: безопасная работа с памятью, отсутствие уязвимостей, отсутствие переполнений, отсутствие нарушений инвариантов в коде, автоматическое отслеживание времени жизни, отсутствие UB и т. п. Как ни печально, полностью валидировать правильную работу приложения в автоматическом режиме просто невозможно:

bool TriggerAirbag(control& c) noexcept {
    return c.car_is_moving() || c.car_is_hit();  // ошибка
}

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

В комитете идёт активная работа по улучшению различных аспектов безопасности языка.

Первое предложение, которое уже длительное время обсуждается в комитете — контракты P2900 [17]. Это возможность показывать инварианты вашего кода компилятору, чтобы он мог проверять время компиляции и время рантайма. Контракты подразумевают ручное проставление:

float sqrt(const float x)
    pre(x >= 0);

Компилятор сможет обнаружить нарушение контракта на этапе компиляции для вызова sqrt(-1.0) и выдать диагностику. Для случаев, когда переменную x нельзя вывести на этапе компиляции, можно заставить компилятор добавлять рантайм проверку и вызывать contract_violation_handler.

И казалось бы, что подобные рантайм-проверки во всех частях кода должны вести к замедлению программы и пользоваться ими в продакшене невозможно. Но тут неожиданно появляется libc++ hardening [18]. Проставление подобных проверок для стандартной библиотеки C++ привело к замедлению на 0,3% (приложение становится медленнее на 1/333), и при этом позволило обнаружить проблемы, которые не отлавливали статические анализаторы и санитайзеры. Разработчики компиляторов поработали на славу, добавив множество оптимизаций, чтобы проверки были легковеснее и оптимизировались лучше.

Hardening очень понравился комитету: есть высокий шанс, что P3471 [19] попадёт в C++26 и появится hardened-режим работы стандартной библиотеки и он будет завязан на предложение по контрактам.

Отдельное предложение по улучшению безопасности кода на C++ — предложение по введению в язык профилей P3081 [20]. Профили — это одновременно и встроенный в компилятор статический анализатор для предотвращения использования опасных конструкций (например, const_cast, аллокаций через new) и одновременно режимы для кодогенерации более надёжного кода с рантайм-проверками (например, на nullptr-разадресацию). На мой взгляд, самые слабые части этого предложения — отсутствие детального описания и отсутствие прототипа.

Есть ещё идеи по анализу времени жизни объектов на этапе компиляции, но они очень далеки от внедрения — в C++26 мы их точно не увидим.

По-прежнему идёт работа по старым направлениям. Продолжается борьба с undefined behavior (UB) в разных частях стандарта. Так, наше предложение от Рабочей Группы 21 [21] по уничтожению UB при работе с placement new P3006 [22] прошло обсуждение в EWG и есть все шансы увидеть его C++26. Для нас это предложение особо важно, так как позволяет зафиксировать текущее поведение компиляторов, например, в проекте 🐙 userver [23], где из-за различных их ограничений нет возможности использовать std::launder (который ни одному из компиляторов и не нужен в этом месте!).

Сколько бит в байте?

Казалось бы, простой вопрос, но есть нюанс…

Ответ

В байте CHAR_BIT бит. В 99,9% случаев, это именно 8. Но есть особые платформы, некоторые из которых до сих пор выпускаются и продаются, где в байте другое число бит. Например в TMS320C28x-процессорах в байте 16 бит.

В предложении P3477 [24] идёт работа над тем, чтобы зафиксировать в C++, что в байте именно 8 бит. Это немного упростит стандарт языка и позволит разработчикам не пользоваться CHAR_BIT.

А лично я думаю, что...

… не стоит закрывать дверь для 0,1% систем с нетипичными архитектурами. Более того, есть что-то заманчивое в том, чтобы позволять и дальше разработчикам железа подобные эксперименты. Например, если минимально адресуемым куском данных сделать 32 бита, то уйдут проблемы unaligned data. Так можно будет сэкономить горсть транзисторов при работе с адресами, что в свою очередь позволит экономить батарейку (кажется, в TMS320C28x как раз для этого и делали 16-бит в байте). А ведь приятно, если космический аппарат сможет пролететь на имеющейся батарейке на пару миллиардов километров больше и передать больше данных из далёкого космоса). А что вы думаете?

Итоги

C++26 уже не за горами. До feature freese остаётся всего четыре встречи комитета: сейчас уже закрывается дверь для новых предложений, одновременно затрагивающих стандартную библиотеку и ядро языка.

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

Если у вас есть важные замечания к стандарту, пожалуйста, делитесь ими на https://stdcpp.ru [25]. А если хотите пообщаться по поводу placement new в userver, приходите на ночь опенсорс-библиотек [26], будет весело!

Автор: antoshkka

Источник [27]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-3/403380

Ссылки в тексте:

[1] P2996: https://wg21.link/P2996

[2] открытая: https://github.com/bloomberg/clang-p2996

[3] P3068: https://wg21.link/P3068

[4] P3309: https://wg21.link/P3309

[5] P2686: https://wg21.link/P2686

[6] P3508: https://wg21.link/P3508

[7] P3369: https://wg21.link/P3369

[8] P1928: https://wg21.link/P1928

[9] P1061: https://wg21.link/P1061

[10] P2662: https://wg21.link/P2662

[11] P3019: https://wg21.link/P3019

[12] P3138: https://wg21.link/P3138

[13] P2897: https://wg21.link/P2897

[14] P3222: https://wg21.link/P3222

[15] P3050: https://wg21.link/P3050

[16] P3355: https://wg21.link/P3355

[17] P2900: https://wg21.link/P2900

[18] libc++ hardening: https://libcxx.llvm.org/Hardening.html

[19] P3471: https://wg21.link/P3471

[20] P3081: https://wg21.link/P3081

[21] Рабочей Группы 21: https://stdcpp.ru/

[22] P3006: https://wg21.link/P3006

[23] 🐙 userver: https://github.com/userver-framework/userver

[24] P3477: https://wg21.link/P3477

[25] https://stdcpp.ru: https://stdcpp.ru

[26] ночь опенсорс-библиотек: https://events.yandex.ru/events/opensourcenight?utm_source=channels&utm_medium=social&utm_campaign=opensourse_night&utm_content=userver

[27] Источник: https://habr.com/ru/companies/yandex/articles/860308/?utm_source=habrahabr&utm_medium=rss&utm_campaign=860308