Несколько недель назад состоялась встреча международного комитета по стандартизации C++. На ней люди (в основном) не разменивались на мелочи и совершили несколько больших шагов на пути к С++20.
Главные новости:
- Расширению Concepts быть в C++20!
- Ranges, Networking и Coroutines/сопрограммы: выпущены в эксперимент в виде TS.
- Модули: черновик TS готов.
Что всё это значит, как это упростит написание кода и что было ещё — читайте под катом.
Concepts
Замечательная вещь под названием Concepts внесена в черновик будущего стандарта С++20. Это большая радость для разработчиков обобщенных библиотек, использующих идиому SFINAE.
- `*_fast` — сильно соптимизированы, но требуют, чтобы следующие операции возвращали T& и были валидны:
v += data; v -= data; v *= data; v /= data;
- `*_slow` — медленные, но работают со всеми типами данных.
Задача — написать функции `*_optimal`, которые используют версию `*_fast`, если это возможно:
#include <iostream>
template <class Container, class Data>
void compute_vector_fast(Container& v, const Data& data) {
std::cout << "fastn";
// ...
}
template <class Container, class Data>
void compute_vector_slow(Container& v, const Data& data) {
std::cout << "slown";
// ...
}
template <class Container, class Data>
void compute_vector_optimal(Container& v, const Data& data) {
// ??? call `compute_vector_slow(v, data)` or `compute_vector_fast(v, data)` ???
}
Без концептов эта задача, например, решается через `std::enable_if_t` и множество нечитаемого шаблонного кода.
С концептами всё намного проще:
#include <iostream>
template <class T, class Data>
concept bool VectorOperations = requires(T& v, const Data& data) {
{ v += data } -> T&;
{ v -= data } -> T&;
{ v *= data } -> T&;
{ v /= data } -> T&;
};
template <class Container, class Data>
requires VectorOperations<Container, Data>
void compute_vector_optimal(Container& v, const Data& data) {
std::cout << "fastn";
}
template <class Container, class Data>
void compute_vector_optimal(Container& v, const Data& data) {
std::cout << "slown";
}
Концепты позволяют:
- писать более простой шаблонный код,
- выдавать более короткие сообщения об ошибках при использовании неверных шаблонных параметров (в теории, пока что это не так!).
С концептами уже можно поэкспериментировать в GCC, если использовать флаг -fconcepts, например, тут. Вот последний доступный proposal на Concepts.
Ranges TS
Ranges увидят свет в виде технической спецификации. Это значит, что поэкспериментировать с ними можно будет еще до C++20.
С Ranges можно писать `sort(container)` вместо `sort(container.begin(), container.end())`, нужно только заиспользовать нужный namespace.
А еще расширяются возможности стандартных алгоритмов. Например, можно искать быстрее, если мы точно знаем, что элемент содержится в контейнере:
#include <vector>
#include <experimantal/ranges/algorithm>
namespace ranges = std::experimental::ranges;
int main () {
// Функция get_some_values_and_delimiter() фозвращает вектор,
// в котором гарантированно есть число 42
std::vector<int> v2 = get_some_values_and_delimiter();
// Необходимо найти число 42 и отсортировать все элементы, идущие после него:
auto it = ranges::find(v.begin(), ranges::unreachable{}, 42);
ranges::sort(++it, v.end());
}
Нечто подобное Александреску делал для получения супербыстрого поиска.
Любителям SFINAE и обобщённых библиотек Ranges тоже принесут счастье, так как они определяют огромное количество концептов: Sortable, Movable, Copyable, DefaultConstructible, Same…
Можно поэкспериментировать, скачав библиотеку отсюда. Вот последний доступный черновик Ranges.
Networking TS
Все, что необходимо для работы с сокетами (в том числе для асинхронной работы), будет выпущено в эксперимент еще до C++20. В основе Networking TS лежит доработанный и улучшенный ASIO.
Вот пара приятных различий:
- В Networking TS можно передавать move-only callback. В ASIO для этого надо было на свой страх и риск поплясать с
бубноммакросом BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS. Так что если у вас есть функциональный объект с unique_ptr, то его можно спокойно использовать для callback. - Больше constexpr для базовых типов (например, для ip::address).
- Вменяемые способы передачи аллокаторов: можно в класс-callback добавить allocator_type и метод allocator_type get_allocator(); можно специализировать шаблон associated_allocator и описать, какие методы класса надо дергать вместо get_allocator().
Можно поэкспериментировать, скачав библиотеку отсюда. Вот последний доступный черновик Networking.
Coroutines TS
Сопрограммы — это «возможность сохранить текущий стек, переключиться на другой стек и поработать там, а потом вернуться». В основном они используются для создания генераторов и асинхронной работы.
Самый смак получается, если смешать Coroutines TS и Networking TS. Тогда вместо асинхронного нечитабельного кода на +100 строк можно получить то же самое, но на 40 строк:
#include <ctime>
#include <iostream>
#include <string>
#include <experimental/net>
using net = std::experimental::net;
using net::ip::tcp;
std::string make_daytime_string() {
using namespace std; // For time_t, time and ctime;
time_t now = time(0);
return ctime(&now);
}
void start_accept(net::io_context& io_service) {
tcp::acceptor acceptor{io_service, tcp::endpoint(tcp::v4(), 13)};
while (1) {
tcp::socket socket(acceptor.get_io_service());
auto error = co_await acceptor.async_accept(socket, net::co_future);
if (error) break;
std::string message = make_daytime_string();
auto& [error, bytes] = co_await async_write(
socket, net::buffer(message), net::co_future
);
if (error) break;
}
}
int main() {
net::io_context io_service;
io_service.post([&io_service](){
try {
start_accept(io_service);
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
});
io_service.run();
}
Но вот плохие новости: такую интеграцию Coroutines TS и Networking TS в стандарт еще не привнесли. Пока что придется реализовывать ее самим.
С сопрограммами уже можно поэкспериментировать в CLANG-6.0, если использовать флаги -stdlib=libc++ -fcoroutines-ts, например, тут. Вот последний доступный черновик Coroutines.
Модули
Подготовлен черновик TS. Дело сдвинулось и есть шансы увидеть модули уже в течение года!
Однако, как правило, вы используете одни и те же заголовочные фалы в каждом файле cpp. За счет того, что компиляция различных файлов cpp никак не связана друг с другом, при каждой компиляции компилятор разбирает одни и те же заголовочные файлы снова и снова. Именно этот разбор и тормозит (заголовочный файл iostream весит более мегабайта, подключите 20 подобных заголовочных файлов — и компилятору придётся просмотреть и разобрать около 30 мегабайт кода при компиляции одного файла cpp).
И тут на сцену выходят модули! Модуль — это набор файлов, собранных воедино и сохранённых на диск в понятном для компилятора бинарном виде. Таким образом, при подключении модуля компилятор просто считает его с диска в свои внутренние структуры данных (минуя этапы открытия нескольких фалов, парсинга, препроцессинга и некоторых другие вспомогательные этапы).
Дополнительный прирост скорости при компиляции будет получен за счёт того, что в модуле вы явно указываете его публичный интерфейс. То есть компилятор сможет сделать ряд оптимизаций ещё при создании модуля. Это сильно уменьшит затраты оперативной памяти, ускорит поиск подходящих перегруженных функций, структур и т. д. за счёт того, что их попросту будет меньше.
И наконец, финальная стадия сборки проекта — линковка. В данный момент линковщик может тратить много времени на выкидывание одинаковых блоков скомпилированного кода (вы написали функцию inline/force_inline, 100 раз её использовали, а компилятор решил её не встраивать — линкер выкинет 99 скомпилированных тел вашей функции и оставит одно). С модулями такого происходить не должно, поскольку файл модуля не будет «вкомпиливаться» внутрь собранного файла cpp.
Модули в черновике не экспортируют макросы, поэтому будет сложновато использовать их для системных файлов с множеством макросов (`<windows.h>`, я на тебя намекаю!) и поддерживать код, использующий модуль и его старый заголовочный файл (если у вас std::string описан в модуле и в заголовочном файле , то при подключении модуля и заголовочного файла будет multiple definitions, поскольку макрос для include guards не экспортируется из модуля). Это как раз такие модули, за которые вы проголосовали в прошлом посте (ваши голоса мы донесли до комитета).
Вот последний доступный черновик Modules.
Мелочи, принятые в C++20
В C++20 можно будет инициализировать bitfields в описании класса:
struct S {
unsigned x1:8 = 42;
unsigned x2:8 { 42 };
};
Можно будет понимать платформы endianness стандартными методами:
if constexpr (std::endian::native == std::endian::big) {
// big endian
} else if constexpr (std::endian::native == std::endian::little) {
// little endian
} else {
// mixed endian
}
Можно будет инициализировать поля структур, прям как в чистом C:
struct foo { int a; int b; int c; };
foo b{.a = 1, .b = 2};
У лямбд можно будет явно указывать шаблонные параметры:
auto bar = []<class... Args>(Args&&... args) {
return foo(std::forward<Args>(args)...);
};
Заслуги РГ21
На встречу в Торонто мы ездили с несколькими предложениями:
- P0652R0 — конкурентные ассоциативные контейнеры. Комитет хорошо встретил предложение, посоветовал провести эксперименты для улучшения ряда мест, посоветовал улучшения в интерфейсе. Начали работу над следующей версией предложения.
- P0539R1 — integers, размер (количество байт) которых задаётся на этапе компиляции. Возникли споры по поводу интерфейса (указывать шаблонным параметром биты, байты или машинные слова), так что в следующей итерации необходимо будет привести плюсы и минусы различных подходов.
- P0639R0 — наше предложение направить усилия в сторону разработки `constexpr_allocator` вместо `constexpr_string + constexpr_vector`. Встретили крайне благосклонно, проголосовали за. Следующие наши шаги — прорабатывать тему вместе с Давидом.
- P0415R0 — constexpr для std::complex. Предложение было одобрено и теперь находится в подгруппе LWG (вместе с предложением на constexpr для стандартных алгоритмов). Должно быть в скором времени смержено в черновик C++20.
Зачем вообще эти constexpr?В комитете С++ активно работают над идеями рефлексии и метаклассов. Обе эти идеи требуют хорошей поддержки constexpr-вычислений от стандартной библиотеки, так что предложения на добавление constexpr — это в основном задел на будущее, чтобы при принятии в стандарт рефлексии можно было использовать стандартные классы и функции.
Кроме того, ряду библиотек уже нужны constexpr-функции: [1], [2].
Вдобавок нас попросили представить комитету два предложения, непосредственно над написанием которых мы не работали:
- P0457R0 — starts_with и ends_with для строк. Комитет предложил сделать это в виде свободных функций. Один из присутствующих сказал, что у них в компании есть эти методы и их используют чаще, чем остальные алгоритмы вместе взятые. Все с нетерпение ждут новой версии proposal, уже со свободными функциями.
- P0458R0 — функция contains(key) member для классов [unordered_]map/set/multimap/multiset. Комитету идия пришлась по душе, почти отправили в LWG для внедрения в C++20.
На подходе
Обсуждали предложение по форматированию текста, и многим понравились предлагаемые возможности (вероятно, потому, что людям нравится Python):
fmt::format("The answer is {}", 42);
Обсуждали ring_span, который по функциональности напоминает boost::circular_buffer, но не владеет элементами (является view над контейнером).
На подходе битовые операции. Когда их примут, правильным ответом на вопрос «Как подсчитать количество выставленых битов в переменной X?» на собеседованиях станет «std::popcount(X)».
Планы и прочее
РГ21 планирует в ближайшее время написать предложения на std::stacktrace (в качестве прототипа послужит Boost.Stacktrace), доработать предложение на std::shared_library и на экспорт символов из динамических библиотек.
Если у вас есть идеи для C++20, если вы нашли проблемы в C++17/14/11 либо просто хотите подстегнуть разработку той или иной фичи C++ — заходите на сайт рабочей группы stdcpp.ru. Добро пожаловать!
Есть желание помочь с написанием предложений и внести своё имя в историю? Мы подготовили мини-инструкцию по написанию предложений.
Автор: antoshkka