Как стать автором
Обновить
85.31
Wunder Fund
Мы занимаемся высокочастотной торговлей на бирже

Современный C++23/26: концепты, корутины и многое другое в высокопроизводительных службах

Уровень сложностиСредний
Время на прочтение11 мин
Количество просмотров6.8K
Автор оригинала: Ethrynto

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

В этом материале исследованы инновационные возможности современного C++. Здесь мы достаточно глубоко остановимся на C++23 и поговорим об ожидаемых перспективных новшествах C++26. Мы рассмотрим разные темы, в частности — концепты (concept) — механизм C++, позволяющий уточнять требования к типам данных при обобщённом программировании. Так же мы поговорим о корутинах (coroutine) — о революционном подходе к написанию асинхронного кода. Мы затронем и другие заметные изменения, в частности — диапазоны (range), модули (module) и развитие средств конкурентного программирования. Статья разбита на разделы, в каждом из которых раскрываются особенности той или иной возможности, разбирается механизм её функционирования, рассматривается её использование в высокопроизводительной среде, даются примеры и рекомендации по её применению.

Концепты в C++

Что такое «концепт»?

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

В С++23, основываясь на том, что уже сделано в C++20, концепты усовершенствовали, улучшив их интеграцию в стандартную библиотеку, а так же немного повысили удобство их использования. Соответствующие конструкции, используемые для их описания — это не просто «синтаксический сахар». Они позволяют создавать более надёжные кодовые базы и способны помочь компилятору в оптимизации кода, давая ему точные сведения об ограничениях типов.

Как работают концепты?

Концепт — это именованный набор требований. Например, стандартная библиотека даёт в наше распоряжение концепт, выглядящий как std::integral (для целочисленных типов), и концепт std::random_access_range (для диапазонов значений с произвольным доступом). Их можно использовать в объявлениях шаблонов, пользуясь оператором requires, или, прибегнув к сокращённому синтаксису, указывать их в списках шаблонных параметров.

Вот простой пример:

#include <concepts>
#include <vector>
#include <iostream>

// Определяем концепт для типов, которые поддерживают суммирование
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};
// Функция, ограниченная концептом Addable
template <typename T>
requires Addable<T>
T add(T a, T b) {
    return a + b;
}
int main() {
    std::cout << add(5, 3) << "\n";       // Работает: тип int удовлетворяет требованиям Addable
    // std::cout << add("a", "b") << "\n"; // Не работает: тип string не удовлетворяет требованиям Addable
    return 0;
}

Если попытаться передать функции add() тип, который не удовлетворяет требованиям концепта Addable (например — std::string) — компилятор отвергнет такой код, выдав чёткое сообщение об ошибке, что выгодно отличается от таинственных сообщения об ошибках, выдаваемых в подобных ситуациях при работе с шаблонами до появления C++20.

Концепты в С++23

В C++23 концепты не подверглись каким‑либо сильным изменениям. В этом стандарте языка лишь была расширена их экосистема. Например, теперь ограничения в виде концептов применены к большему количеству компонентов стандартной библиотеки. Это упрощает написание переносимого и типобезопасного кода. В частности, концепты интенсивно используются в библиотеке std::ranges. Благодаря этому механизмы из этой библиотеки принимают только диапазоны совместимых типов.

Применение в высокопроизводительных службах

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

#include <ranges>
#include <vector>
#include <algorithm>

template <std::ranges::random_access_range R>
void optimize_sort(R& range) {
    std::ranges::sort(range); // Гарантирована вычислительная сложность O(n log n) на диапазонах с произвольным доступом к элементам
}
int main() {
    std::vector<int> data = {5, 2, 9, 1, 5};
    optimize_sort(data); // Работает нормально
    // std::list<int> list = {1, 2, 3};
    // optimize_sort(list); // Ошибка компиляции: тип list не является диапазоном с произвольным доступом к элементам
    return 0;
}

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

Корутины в С++

Что такое корутины?

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

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

Как работают корутины?

Работа с корутинами в C++ реализуется посредством трёх ключевых слов: co_await, co_yield и co_return. В основе корутин лежит инфраструктура, в состав которой входят promise‑объект (promise type, объект обещания), объект управления корутиной (coroutine handle, дескриптор корутины), а так же awaitable‑объект (awaitable, ожидаемый объект). Вот упрощённый пример:

#include <coroutine>
#include <iostream>

struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};
Task async_operation() {
    std::cout << "Starting async task\n";
    co_await std::suspend_always{}; // Здесь приостановить работу
    std::cout << "Task resumed\n";
}
int main() {
    auto task = async_operation();
    auto handle = task.operator std::coroutine_handle<>();
    std::cout << "Main: Before resume\n";
    handle.resume(); // Возобновить корутину
    std::cout << "Main: After resume\n";
    handle.destroy();
    return 0;
}

Вот что выведет этот код:

Starting async task
Main: Before resume
Task resumed
Main: After resume

Здесь команда co_await std::suspend_always{} приостанавливает корутину, позволяя вызывающей стороне управлять возобновлением её работы с помощью дескриптора корутины. На практике в подобных ситуациях пользуются awaitable‑объектами, привязанными к операциям ввода‑вывода или к таймерам.

Применение в высокопроизводительных службах

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

struct AsyncServer {
    struct Connection {
        struct promise_type {
            Connection get_return_object() { return {}; }
            std::suspend_always initial_suspend() { return {}; }
            std::suspend_always final_suspend() noexcept { return {}; }
            void return_void() {}
            void unhandled_exception() {}
        };
    };

Connection handle_client(int id) {
        std::cout << "Client " << id << " connected\n";
        co_await std::suspend_always{}; // Ожидание поступления данных
        std::cout << "Client " << id << " processed\n";
    }
    void run() {
        std::vector<std::coroutine_handle<>> clients;
        for (int i = 0; i < 3; ++i) {
            clients.push_back(handle_client(i).operator std::coroutine_handle<>());
        }
        for (auto& client : clients) {
            client.resume(); // Имитация поступления данных
            client.destroy();
        }
    }
};
int main() {
    AsyncServer server;
    server.run();
    return 0;
}

Расширение возможностей корутин в С++23

Стандарт C++23 не внёс серьёзных изменений в функционирование корутин. В нём улучшена поддержка этого механизма в стандартной библиотеке, например, обеспечена лучшая интеграция с std::future, кроме того — в нём появились новые вспомогательные средства для управления жизненным циклом корутин. В целом, можно сказать, что эти усовершенствования повысили практическое удобство работы с корутинами в реальных проектах.

Другие ключевые возможности C++23

Диапазоны

Библиотека std::ranges, появившаяся в C++20 и расширенная в C++23, предлагает программисту современный способ работы с последовательностями данных. В С++23 добавлены новые представления (view) и адаптеры (adaptor), расширяющие возможности компоновки операций. Например:

#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};
    auto even_squares = nums | std::ranges::views::filter([](int x) { return x % 2 == 0; })
                            | std::ranges::views::transform([](int x) { return x * x; });
    for (int n : even_squares) {
        std::cout << n << " "; // Outputs: 4 16
    }
    std::cout << "\n";
    return 0;
}

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

Модули

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

Пишем модуль:

export module math;

export int add(int a, int b) {
    return a + b;
}

Импортируем его:

import math;
#include <iostream>

int main() {
    std::cout << add(2, 3) << "\n"; // Outputs: 5
    return 0;
}

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

Улучшения, касающиеся constexpr

В C++23 расширены возможности ключевого слова constexpr, благодаря чему больше функций из стандартной библиотеки (например — кое‑что из заголовочного файла <algorithm>) можно запускать во время компиляции программы. Это позволяет произвести некоторые вычисления не во время выполнения, а во время компиляции программы, что благотворно сказывается на производительности:

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int result = factorial(5); // Вычислено во время компиляции: 120
    std::cout << result << "\n";
    return 0;
}

Улучшения в сфере конкурентного выполнения кода

В C++23 представлены улучшения инструментов вроде std::jthread (из C++20) и усовершенствованы примитивы синхронизации. Всё это жизненно важно для написания кода, работающего на многоядерных CPU в составе приложений, требовательных к производительности.

Возможности, появление которых ожидается C++26

Рефлексия

Статическая рефлексия (static reflection), появление которой ожидается в C++26, позволит проводить интроспекцию типов и их членов во время компиляции кода. Это может позволить генерировать код для сериализации данных или для целей оптимизации программ:

// Возможный синтаксис
template <typename T>
void print_members(T obj) {
    for_each(reflexpr(T)::members) | [](auto member) {
        std::cout << member.name << ": " << obj.*member << "\n";
    };
}

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

Сопоставление с образцом

Сопоставление с образцом (pattern matching) — это метод анализа и обработки структур данных, на возможное появление которого в C++ оказали влияние функциональные языки. Его применение позволяет упростить сложную управляющую логику:

 // Возможный синтаксис
std::variant<int, std::string> v = 42;
inspect (v) {
    int i => std::cout << "Int: " << i << "\n";
    std::string s => std::cout << "String: " << s << "\n";
}

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

Гетерогенные вычисления

В C++26 может быть стандартизирована поддержка написания программ для GPU, основанная на таких библиотеках, как SYCL. Это позволит удобно переносить нагрузки, требующие большой вычислительной мощности, на видеоускорители. Такие нагрузки характерны для задач симуляции и для служб, реализующих алгоритмы машинного обучения.

Расширение возможностей корутин

В новом стандарте ожидается появление доработок, касающихся корутин. Среди них — улучшение поддержки корутин, не использующих отдельный стек, а так же — улучшение интеграции с библиотеками. Это позволит ещё качественнее оптимизировать решение асинхронных задач.

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

Комбинация вышеописанных возможностей позволяет достичь мощного синергетического эффекта. Например — можно воспользоваться концептами для описания ограничений обобщённого сервера, основанного на корутине:

template <typename Handler>
requires std::invocable<Handler, int>
Task process_request(Handler h, int data) {
    co_await h(data);
}

Диапазоны можно использовать при обработке данных, а модули позволят разбить код на удобные фрагменты. Всё вместе это позволит создать отлично масштабируемую, эффективную систему. Правда, всё хорошо в меру. Например, корутины, если ими злоупотреблять, создают чрезмерную дополнительную нагрузку на систему. А неоправданное увлечение метапрограммированием, основанном на шаблонах, способно сильно увеличить время, необходимое на компилирование программы. Применяя новые возможности серьёзно подходите к профилированию и тестированию своих программ.

Итоги

Стандарты C++23 и C++26 дают разработчикам инструменты для создания высокопроизводительных служб, которые отличаются высокой скоростью работы и надёжностью, которые легко поддерживать. Концепты позволяют обеспечивать корректность шаблонных параметров. Корутины упрощают конкурентное программирование, а возможности вроде диапазонов и модулей оптимизируют использование ресурсов. По мере эволюции C++ использование новых возможностей языка позволит поддерживать конкурентоспособность систем в условиях возрастающих требований к производительности и надёжности. Изучайте C++, экспериментируйте и в полную силу используйте его новые возможности.

О, а приходите к нам работать? 🤗 💰

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде

Теги:
Хабы:
+11
Комментарии33

Публикации

Информация

Сайт
wunderfund.io
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия
Представитель
xopxe