habrahabr

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

  • вторник, 3 декабря 2024 г. в 00:00:18
https://habr.com/ru/companies/yandex/articles/860308/
В конце ноября состоялась встреча международного комитета по стандартизации языка программирования C++.



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


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


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

Сейчас есть как минимум три имплементации. Одна из них открытая. Некоторые из имплементаций доступны на 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)) == type_of(^^std::string));

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

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

constexpr


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

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

Атомарные типы atomic<T> и atomic_ref<T> тоже стали constexpr в P3309. Теперь, например, можно писать многопоточный 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. Теперь constexpr static auto [v0, v1] = A{2, 3}; скомпилируется.

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

SIMD


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

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 можно раскладывать кортежи и агрегаты произвольных размеров на отдельные поля, а вместе с принятым ранее P2662 удобно к ним обращаться:

[](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. Первый предназначен для хранения объекта по указателю. При этом его копирование приводит к копированию объекта, на который он ссылается. Полезная штука для создания рекурсивных структур и вынесения больших подобъектов в динамическую память, чтобы сам объект мог умещаться на стеке. std::polymorphic<T> работает аналогично, но позволяет хранить в себе наследников от T, обеспечивая их правильное копирование.

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

Итоги


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

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

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