На шаг ближе к С++20. Итоги встречи в Торонто

antoshkka 28 сентября в 22:47 22,7k
Несколько недель назад состоялась встреча международного комитета по стандартизации C++. На ней люди (в основном) не разменивались на мелочи и совершили несколько больших шагов на пути к С++20.

image

Главные новости:

  • Расширению Concepts быть в C++20!
  • Ranges, Networking и Coroutines/сопрограммы: выпущены в эксперимент в виде TS.
  • Модули: черновик TS готов.

Что всё это значит, как это упростит написание кода и что было ещё — читайте под катом.

Concepts


Замечательная вещь под названием Concepts внесена в черновик будущего стандарта С++20. Это большая радость для разработчиков обобщенных библиотек, использующих идиому SFINAE.

Мотивирующий пример для любителей SFINAE
В вашей библиотеке есть функции `*_fast` и `*_slow`, принимающие на вход два шаблонных параметра `v` и `data`:

  1. `*_fast` — сильно соптимизированы, но требуют, чтобы следующие операции возвращали T& и были валидны:

        v += data;
        v -= data;
        v *= data;
        v /= data;
    
  2. `*_slow` — медленные, но работают со всеми типами данных.

Задача — написать функции `*_optimal`, которые используют версию `*_fast`, если это возможно:

#include <iostream>

template <class Container, class Data>
void compute_vector_fast(Container& v, const Data& data) {
    std::cout << "fast\n";
    // ...
}

template <class Container, class Data>
void compute_vector_slow(Container& v, const Data& data) {
    std::cout << "slow\n";
    // ...
}

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 << "fast\n";
}

template <class Container, class Data>
void compute_vector_optimal(Container& v, const Data& data) {
    std::cout << "slow\n";
}


Концепты позволяют:

  • писать более простой шаблонный код,
  • выдавать более короткие сообщения об ошибках при использовании неверных шаблонных параметров (в теории, пока что это не так!).

С концептами уже можно поэкспериментировать в 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


Сопрограммы — это «возможность сохранить текущий стек, переключиться на другой стек и поработать там, а потом вернуться». В основном они используются для создания генераторов и асинхронной работы.
Уточнение от @masterspline
На самом деле, в стандарт вошла версия от MS, которая stackless. Она никакие стеки не переключает. По сути вошедшая в стандарт версия корутин — это функтор (функциональный объект C++, типа лямбды), который создается компилятором из функции, помеченной как «корутина». Свое состояние stackless корутина, как и функтор, держат в членах класса, а для реализации возможности многократного вызова функции-корутины, определен operatop()(), реализованный в виде конечного автомата типа Duff's device в начале которого идет switch() по текущему состоянию конечного автомата.

Самый смак получается, если смешать 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. За счет того, что компиляция различных файлов 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. Добро пожаловать!

Есть желание помочь с написанием предложений и внести своё имя в историю? Мы подготовили мини-инструкцию по написанию предложений.
Проголосовать:
+104
Сохранить: