Русский
Русский
English
Статистика
Реклама

Ranges

Стандарт C20 обзор новых возможностей C. Часть 4 Ranges

19.05.2021 14:18:09 | Автор: admin


25 февраля автор курса Разработчик C++ в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас, и чем они могут быть полезны.

При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным. Он растянулся почти на 2,5 часа. Для вашего удобства текст мы разбили на шесть частей:

  1. Модули и краткая история C++.
  2. Операция космический корабль.
  3. Концепты.
  4. Ranges.
  5. Корутины.
  6. Другие фичи ядра и стандартной библиотеки. Заключение.

Это четвёртая часть, рассказывающая о новом модуле стандартной библиотеки, Ranges.

Ranges


До этого рассматривалось ядро языка. Теперь я расскажу про изменение в стандартной библиотеке, которое не добавляет нового в синтаксис, введённый C++20 заголовочный файл <ranges>.



Мотивация


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

Слушателям предлагался вопрос. Сколько строк выведет код ниже?

  1. 5
  2. 6
  3. 9
  4. Ни одной.
  5. Будет выводить, пока не остановим.

Подумайте, прежде чем прочитать ответ.

#include <iostream>int main() {    const int days = 3;   // количество дней с играми    const int games = 2;  // количество игр с питомцем в день    for (int i = 0; i < days; i++) {        std::cout << "День " << i << std::endl;        for (int j = 0; j < games; i++) {            std::cout << "  Игра " << j << std::endl;        }    }}

Во время вебинара правильных ответов оказалось больше, чем я ожидал. Это тест на внимательность, потому что в коде есть ошибка: цикл никогда не завершится. Проблема в том, что я перепутал инкрементируемую переменную во внутреннем цикле. Допустить такую ошибку куда проще, чем её заметить. Если вы ни разу так не ошибались, вряд ли вы программируете на C/C++.

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

std::merge(  collection_holder.get_left_collection().begin(),   collection_holder.get_left_collection().end(),   collection_holder.get_right_collection().begin(),   collection_holder.get_right_collection().end(),  std::back_inserter(    collection_holder.get_destination_collection()  ));

Ошибиться в такой конструкции несложно. Код стал бы более выразительным, если бы вместо пары итераторов передавался один диапазон.

Ещё один пример для мотивации. Для стандартных действий есть алгоритм, скажем, copy_if, который копирует элементы, удовлетворяющие условию. Он используется для фильтрации элементов. Алгоритм transform применяет к каждому элементу функцию. Предположим, нужно выполнить обе операции: отфильтровать элементы и к оставшимся применить функцию. Такой алгоритм можно назвать transform_if, но к сожалению, в стандартной библиотеке его нет.

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

std:vector<int> numbers_in;std:vector<int> numbers_out;// задача: поделить на 2 все чётные числа из numbers_in// и записать результаты numbers_outstd:vector<int> intermediate;// скопируем в intermediate только чётные числаstd::copy_if(numbers_in.begin(), numbers_in.end(),               std::back_inserter(intermediate),              [](int x) {                        return x % 2 == 0;             });// поделим их на 2std::transform(intermediate.begin(), intermediate.end(),               std::back_inserter(numbers_out),               [](int x) {                          return x / 2;               })

Тут пришлось завести промежуточное хранилище. Получили неэффективное решение с двумя проходами по элементам.

Что у других


В Python есть классная фича: можно писать прямо в выражениях квадратные скобки и делать внутри всё что угодно, в том числе фильтровать элементы и применять к ним функции. Это замечательно и удобно. Возможно, Ranges в C++ делали, поглядывая на Python.

Пример с transform_if сокращается до одной строки:

# transform_if в одну строку средствами языка:numbers_out = [x // 2 for x in numbers_in if x % 2 == 0]

Обычные циклы упрощаются в Python. Пример, в котором я допустил ошибку, мог бы выглядеть так:

days = 3   # количество дней с играмиgames = 2  # количество игр с питомцем в деньfor i in range(days):    print("День %d" % i)    for j in range(games):        print("  Игра %d" % j)

В Python не нужно писать i = 0; i < N; ++i. Буква i набирается один раз, и возможности перепутать что-либо у вас нет. Кстати говоря, range даёт не контейнер, который содержит все элементы, а генерирует числа на лету. По производительности это если и будет уступать обычному циклу, то едва-едва.

Приведу преимущества Python списком:

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

Другой пример SQL. Хотя это не язык программирования, мы можем реализовать transform_if даже на нём:

SELECT n / 2 FROM tab WHERE n % 2 == 0;

Код получается вполне выразительным.

Примеры


В C++20 появилась возможность решать проблемы, озвученные в начале статьи, это библиотека Ranges. Например, так выглядят циклы привычные for, которые перебирают все целые числа в некотором интервале:

#include <iostream>#include <ranges>namespace rng = std::ranges;namespace view = rng::views;int main() {    const int days = 3;   // количество дней с играми    const int games = 2;  // количество игр с питомцем в день    for (int i : view::iota(0, days)) {        std::cout << "День " << i << std::endl;        for (int j : view::iota(0, games)) {            std::cout << "  Игра " << j << std::endl;        }    }}

Теперь не придётся три раза писать i, три раза писать j, и нельзя их перепутать. Вместо привычных циклов с инкрементом итерируемся по std::ranges::views::iota. Как и в Python, функция не будет генерировать контейнер, а выдаст числа на лету.

Range упрощает реализацию transform_if:

#include <iostream>#include <ranges>#include <vector>namespace rng = std::ranges;namespace view = rng::views;int main() {    auto even = [](int i) { return i % 2 == 0; };    auto half = [](int i) { return i / 2; };        std::vector<int> numbers_in = {100, 55, 80, 2, -1};    auto numbers_out = numbers_in | view::filter(even) |  // <-- вся магия                 view::transform(half);        for (auto i : numbers_out) {        std::cout << i << std::endl; // 50, 40, 1    }}

Здесь различные преобразования комбинированы операцией |, или как её ещё называют, pipe. filter, отфильтровывающий нечётные числа, комбинирован с transform, делящим на два. Получаем объект-диапазон, по которому можно итерировать. Этот объект ленивый: он не будет ничего вычислять, пока вы не запросите первое значение. Более того, вернув первое значение, диапазон не будет вычислять второе до тех пор, пока оно не будет запрошено. Это позволяет не хранить в памяти все значения единовременно и применять алгоритмы в один проход. Замечательная функция.

Но если всё же вам необходимо вычислить всё сразу, вы можете по старинке сложить все элементы диапазона в какой-нибудь контейнер: numbers_vector = std::vector(number_out.begin(), numbers_out.end()).

На этом слайде приведено сравнение, как это пишется в C++20, а как в Python. Видно, что Python по-прежнему лаконичнее, но всё-таки C++ уже гораздо ближе.



Такие возможности появились в C++20, и это классно. Разумный вопрос: а что с производительностью?

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

В варианте с вызывается функция iota. Каждую итерацию будет что-то вычисляться. Возможно, будет оверхед.

Я сделал бенчмарк для сравнения цикла в старом стиле с itoa. Его результат на слайде:



Различия в производительности нет! Оказывается, что цикл с itoa настолько же эффективен, как и простой с инкрементом переменной.

При добавлении сложного синтаксического сахара производительность часто падает. Но только не в C++.

Ещё один бенчмарк, в котором сравнивается transform_if в старой реализации и в новой на основе <ranges>.



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

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

Ещё примеры


Это было только начало. В Ranges ещё масса интересного.

Вот код, который сортирует элементы вектора и выводит их:

#include <iostream>#include <vector>#include <algorithm>#include <ranges>namespace rng = std::ranges;template <rng::input_range Range>void Print(const Range& range) {    std::cout << "Elements:";    for (const auto& x : range) {        std::cout << ' ' << x;    }    std::cout << std::endl;}int main() {    std::vector v = { 4, 1, 7, 2, 3, 8 };    rng::sort(v);    Print(v); // Elements: 1 2 3 4 7 8    return 0;}

Заметьте, что здесь применяется не обычный алгоритм std::sort, а sort из пространства имён std::ranges. В этот алгоритм можно передать не пару итераторов, а контейнер то, чего и хотелось.

Я написал функцию Print, которая использует возможности концептов. Используется концепт input_range из пространства имён std::ranges. Он нужен в шаблонной функции для того, чтобы она принимала только объекты-диапазоны с точки зрения Ranges.

В C++20 этот код можно упростить:

void Print(const rng::input_range auto& range) {    std::cout << "Elements:";    for (const auto& x : range) {        std::cout << ' ' << x;    }    std::cout << std::endl;}

Слово template убирается и становится неявным. А const auto& это тип параметра, к нему применён концепт input_range.

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

struct Lecture {    int course;    int local_idx;    int complexity;};std::vector<Lecture> ReadLectures();int main() {    std::vector<Lecture> lectures = ReadLectures();    // как раньше    std::sort(lectures.begin(), lectures.end(),          [](const Lecture& lhs, const Lecture& rhs) {            return lhs.complexity < rhs.complexity;    });    return 0;}

Пришлось разработать свой компаратор и дублировать название поля complexity. В <ranges> добавлены новые реализации старых алгоритмов, которые помимо передачи диапазона допускают параметр-проекцию. Такой алгоритм применит заданную вами функцию и сравнит её результаты вместо сравнения элементов контейнера напрямую:

struct Lecture {    int course;    int local_idx;    int complexity;};std::vector<Lecture> ReadLectures();namespace rng = std::ranges;int main() {    std::vector<Lecture> lectures = ReadLectures();    // как теперь    rng::sort(lectures, std::less<>{}, [](const Lecture& x) {        return x.complexity;    });    return 0;}

В качестве компаратора взят обычный std::less, который сравнивает, применяя операцию <, но благодаря проекции применяется он не к элементам вектора, а к значениям лямбда-функции.

Теория


На этом слайде приведены новые алгоритмы из пространства имён std::ranges. Работа Комитета впечатляет: 85 алгоритмов, каждый из которых тщательно проработан.



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

Вернёмся к примеру с transform_if.

auto range = numbers_in | view::filter(even) |             view::transform(half);

Мы отфильтровали вектор numbers_in и применили к его элементам функцию. Имена filter и transform примеры адаптеров, то есть таких объектов Ranges, которые меняют диапазон.

В библиотеке есть несколько видов адаптеров. Адаптер drop отбрасывает элементы, а take ограничивает их количество. Предположим, нам нужны элементы с 5-го по 14-й. На SQL это было бы сделано так:

SELECT * FROM tab LIMIT 10 OFFSET 5

На C++ теперь можно сделать похожим образом:

using namespace view = rng::views;for (const auto& x : tab | view::drop(5) | view::take(10)) {    std::cout << x << std::endl;}

Адаптеров в стандартной библиотеке не так много, как алгоритмов, а жаль. Вот список всех: all, filter, transform, take, take_while, drop, drop_while, join, split, counted, common, reverse, elements, keys, values.

Зато можно создавать свои! Этот пример кода я не буду разбирать подробно. Посмотрите, как просто мы создали адаптеры для выделения разных категорий пользователей и комбинировали их:

auto ByGender(Gender gender) {    return view::filter([gender](const Person& person) {        return person.gender == gender;     });}auto ByEmployment(bool is_employed) {    return view::filter([is_employed](const Person& person) {        return person.is_employed == is_employed;     });}template <rng::ForwardRange Range>AgeStats ComputeStats(Range&& persons) {    auto females = ByGender(Gender::FEMALE);    auto males = ByGender(Gender::MALE);      auto employed = ByEmployment(true);    auto unemployed = ByEmployment(false);    return {        ComputeMedianAge(persons),        ComputeMedianAge(persons | females),        ComputeMedianAge(persons | males),            ComputeMedianAge(persons | females | unemployed),        ComputeMedianAge(persons | males | employed)     };}

Статус




Ranges это нововведение стандартной библиотеки. У каждого компилятора есть своя родная реализация.

  • К сожалению, в библиотеке Clang диапазоны пока не реализованы.
  • В библиотеке Visual Studio 2019 присутствует поддержка <ranges>, но частично. Например, нет некоторых стандартных алгоритмов.
  • В библиотеке GCC уже всё хорошо, и <ranges> можно использовать смело.

Они неспроста помечены в стандартной библиотеке GCC как экспериментальные. Прямо сейчас готовится десяток ломающих улучшений, Ranges активно дорабатываются и переделываются уже после релиза. Лучше ближайшие пару лет ими не пользоваться, если нет желания исправлять код с новым релизом компилятора.
Антон Полухин

Заключение


Во время вебинара мы спросили у аудитории, нравится ли эта функция. Результаты опроса:

  • Суперфича 21 (80,77%)
  • Так себе фича 2 (7,69%)
  • Пока неясно 3 (11,54%)

Больше всего разочаровывает в Ranges небольшое количество стандартных адаптеров. Например, в Python есть функция enumerate, которая к элементу приписывает его порядковый номер. Там есть возможность итерироваться по декартову произведению парам элементов разных диапазонов. Есть zip, при котором итерирование будет происходить по диагонали декартова произведения. Ничего подобного в C++ я не нашёл. Будем надеяться на новые выпуски Стандарта.
Их добавляют в C++23.
Антон Полухин
Но в целом мне диапазоны нравятся, хотя я понимаю, почему некоторые зрители вебинара не оценили их. Наверное, многим это нововведение кажется сложным. Но мы привыкнем ведь пишем на C++ и разбирались и не с таким.

Одно могу сказать точно: Ranges изменит стиль программирования очень многих вещей. И это здорово, потому что код станет более понятным, а программы более надёжными!

Читателям Хабра, как и слушателям вебинара, дадим возможность оценить нововведения.
Подробнее..

Отладочный вывод на микроконтроллерах как Concepts и Ranges отправили мой printf на покой

09.05.2021 22:19:11 | Автор: admin

Здравствуйте! Меня зовут Александр и я работаю программистом микроконтроллеров.

Начиная на работе новый проект, я привычно набрасывал в project tree исходники всяческих полезных утилит. И на хедере app_debug.h несколько подзавис.

Дело в том, что в декабре прошлого года у GNU Arm Embedded Toolchain вышел релиз 10-2020-q4-major, включающий все GCC 10.2 features, а значит и поддержку Concepts, Ranges, Coroutines вкупе с другими, менее "громкими" новинками С++20.

Воодушевленное новым стандартом воображение рисовало мой будущий С++ код ультрасовременным и лаконично-поэтичным. И старый, добрый printf("Debug message\n") в это благостное видение не очень-то вписывался.

Хотелось бескомпромиссной плюсовой функциональности и стандартных удобств!

float raw[] = {3.1416, 2.7183, 1.618};array<int, 3> arr{123, 456, 789};cout << int{2021}       << '\n'     << float{9.806}    << '\n'     << raw             << '\n'     << arr             << '\n'     << "Hello, Habr!"  << '\n'     << ("esreveR me!" | views::take(7) | views::reverse ) << '\n';

Ну а если хочется хорошего, зачем же себе отказывать?

Реализуем на С++20 интерфейс потока для отладочного вывода МК, поддерживающий любой подходящий протокол, предусмотренный вендром камня. Легковесный и быстрый, без бойлерплейта. Поддерживающий как блокирующий посимвольный вывод - для нечувствительных к времени выполнения участков кода, так и неблокирующий, для быстрых функций.

Зададим для комфортного чтения кода несколько удобных алиасов:

using base_t = std::uint32_t;using fast_t = std::uint_fast32_t;using index_t = std::size_t;

Как известно, в микроконтроллерах неблокирующие алгоритмы передачи данных реализуются на прерываниях и DMA. Для идентификации режимов вывода заведем enum:

enum class BusMode{BLOCKING,IT,DMA,};

Опишем базовый класс, реализующий логику протоколов, ответственных за отладочный вывод:

class BusInterface
template<typename T>class BusInterface{public:using derived_ptr = T*;    static constexpr BusMode mode = T::mode;void send (const char arr[], index_t num) noexcept {if constexpr (BusMode::BLOCKING == mode){derived()->send_block(arr, num);} else if (BusMode::IT == mode){derived()->send_it(arr, num);} else if (BusMode::DMA == mode){derived()->send_dma(arr, num);}}private:derived_ptr derived(void) noexcept{return static_cast<derived_ptr>(this);}void send_block (const char arr[], const index_t num) noexcept {}void send_it (const char arr[], const index_t num) noexcept {}void send_dma (const char arr[], const index_t num) noexcept {}};

Класс реализован по паттерну CRTP, что дает нам преимущества полиморфизма времени компиляции. Класс содержит единственный публичный метод send(), в котором на этапе компиляции, в зависимости от режима вывода, выбирается нужный метод. В качестве аргументов метод принимает указатель на буфер с данными и его полезный размер. На моей практике это самый распространенный формат аргументов в HAL-функциях вендоров МК.

И тогда например класс Uart, наследуемый от данного базового класса, будет выглядеть примерно так:

class Uart
template<BusMode Mode>class Uart final : public BusInterface<Uart<Mode>> {private:static constexpr BusMode mode = Mode;void send_block (const char arr[], const index_t num) noexcept{HAL_UART_Transmit(&huart,bit_cast<std::uint8_t*>(arr),std::uint16_t(num),base_t{5000});}    void send_it (const char arr[], const index_t num) noexcept {HAL_UART_Transmit_IT(&huart,bit_cast<std::uint8_t*>(arr),std::uint16_t(num));}void send_dma (const char arr[], const index_t num) noexcept {HAL_UART_Transmit_DMA(&huart,bit_cast<std::uint8_t*>(arr),std::uint16_t(num));}friend class BusInterface<Uart<BusMode::BLOCKING>>;friend class BusInterface<Uart<BusMode::IT>>;friend class BusInterface<Uart<BusMode::DMA>>;};

По аналогии можно реализовать классs и других протоколов, поддерживаемых микроконтроллером, заменив в методах send_block(), send_it() и send_dma() соответствующие функции HAL. Если протокол передачи данных поддерживает не все режимы, тогда соответствующий метод просто не определяем.

И в завершении этой части заведем короткие алиасы итогового класса Uart:

using UartBlocking = BusInterface<Uart<BusMode::BLOCKING>>;using UartIt = BusInterface<Uart<BusMode::IT>>;using UartDma = BusInterface<Uart<BusMode::DMA>>;

Отлично, теперь разработаем класс потока вывода:

class StreamBase
template <class Bus, char Delim>class StreamBase final: public StreamStorage{public:using bus_t = Bus;  using stream_t = StreamBase<Bus, Delim>;static constexpr BusMode mode = bus_t::mode;StreamBase() = default;~StreamBase(){ if constexpr (BusMode::BLOCKING != mode) flush(); }  StreamBase(const StreamBase&) = delete;StreamBase& operator= (const StreamBase&) = delete;stream_t& operator << (const char_type auto c){if constexpr (BusMode::BLOCKING == mode){bus.send(&c, 1);} else {*it = c;it = std::next(it);}return *this;}stream_t& operator << (const std::floating_point auto f){if constexpr (BusMode::BLOCKING == mode){auto [ptr, cnt] = NumConvert::to_string_float(f, buffer.data());bus.send(ptr, cnt);} else {auto [ptr, cnt] = NumConvert::to_string_float(f, buffer.data() + std::distance(buffer.begin(), it));it = std::next(it, cnt);}return *this;}stream_t& operator << (const num_type auto n){auto [ptr, cnt] = NumConvert::to_string_integer( n, &buffer.back() );if constexpr (BusMode::BLOCKING == mode){bus.send(ptr, cnt);} else {auto src = std::prev(buffer.end(), cnt + 1);it = std::copy(src, buffer.end(), it);}return *this;}stream_t& operator << (const std::ranges::range auto& r){        std::ranges::for_each(r, [this](const auto val) {                        if constexpr (char_type<decltype(val)>){                            *this << val;            } else if (num_type<decltype(val)> || std::floating_point<decltype(val)>){                *this << val << Delim;            }        });return *this;}private:void flush (void) {bus.send(buffer.data(), std::distance(buffer.begin(), it));it = buffer.begin();}std::span<char> buffer{storage};std::span<char>::iterator it{buffer.begin()};bus_t bus;}; 

Рассмотрим подробнее его значимые части.

Шаблон класса параметризуется классом протокола, значением Delim типа char и наследуется от класса StreamStorage. Единственная задача последнего - предоставить доступ к массиву char, в котором будут формироваться строки вывода в неблокирующем режиме. Имплементацию здесь не привожу, она вторична к рассматриваемой теме; оставляю на ваше усмотрение или утяните из моего примера в конце статьи. Для удобной и безопасной работы с этим массивом (в примере - storage) мы заведем два приватных члена класса:

std::span<char> buffer{storage};std::span<char>::iterator it{buffer.begin()};

Delim - разделитель между значениями чисел при выводе содержимого массивов/контейнеров.

Публичные методы класса - это четыре перегрузки operator<<. Три из них - для вывода базовых типов, с которыми наш интерфейс будет работать (char, float и integral type), а четвертая - для вывода содержимого массивов и стандартных контейнеров.

Вот здесь начинается самая вкуснота.

Каждая перегрузка оператора вывода - фактически шаблонная функция, в которой шаблонный параметр ограничен требованиями указанного концепта. Я использую собственные концепты char_type, num_type...

template <typename T>concept char_type = std::same_as<T, char>;template <typename T>concept num_type = std::integral<T> && !char_type<T>;

... и концепты из стандартной библиотеки - std::floating_point и std::ranges::range.

Концепты базовых типов защищают нас от неоднозначных перегрузок, и в комплексе с концептом range позволяет нам реализовать единый алгоритм вывода для любых стандартных контейнеров и массивов.

Логика внутри каждого оператора вывода базового типа проста. В зависимости от режима вывода (блокирующий / не блокирующий) мы или сразу отправляем символ на печать, либо формируем в буфере потока строку. И в момент выхода из функции объект нашего потока разрушается, вызывается деструктор, где приватный метод flush() отправляет заготовленную строку на печать в режиме IT или DMA.

При конвертации числового значения в массив char-ов я отказался от известной идиомы с snprintf() в пользу наработок neiver. Автор в своих публикациях показывает заметное превосходство предложенных им алгоритмов конвертации чисел в строку как в размере бинарника, так и в скорости преобразования. Позаимствованный у него код я инкапсулировал в классе NumConvert, содержащем методы to_string_integer() и to_string_float().

В перегрузке оператора вывода данных массива/контейнера мы с помощью стандартного алгоритма std::ranges::for_each() пробегаемся по содержимому рэйнджа и если элемент удовлетворяет концепту char_type, выводим строку слитно. Если же удовлетворяет концептам num_type или std::floating_point, разделяем значения с помощью заданного значения Delim.

Ну хорошо, мы тут наворотили шаблонов, концептов и прочей плюсовой тяжелой артиллерии. Это ж какой длины мы получим ассемблерную портянку на выходе? Посмотрим два примера:

int main() {    using StreamUartBlocking = StreamBase<UartBlocking, ' '>;    StreamUartBlocking cout;    cout << 'A'; // 1  cout << ("esreveR me!" | std::views::take(7) | std::views::reverse); // 2    return 0;}

Выставим флаги компилятора: -std=gnu++20 -Os -fno-exceptions -fno-rtti. Тогда на первом примере мы получим следующий ассемблерный листинг:

main:        push    {r3, lr}        movs    r0, #65        bl      putchar        movs    r0, #0        pop     {r3, pc}

На втором:

.LC0:        .ascii  "esreveR me!\000"main:        push    {r3, r4, r5, lr}        ldr     r5, .L4        movs    r4, #5.L3:        subs    r4, r4, #1        bcc     .L2        ldrb    r0, [r5, r4]    @ zero_extendqisi2        bl      putchar        b       .L3.L2:        movs    r0, #0        pop     {r3, r4, r5, pc}.L4:        .word   .LC0

На мой взгляд, весьма неплохо. Мы получили привычный плюсовой интерфейс потока, удобный вывод числовых значений, контейнеров/массивов, обработку рэнджей прямо в сигнатуре вывода и все это с фактически нулевым оверхедом.

Конечно же, при выводе числовых значений, добавится еще код конвертации числа в строку.

Потестировать онлайн можно здесь (hardware dependent код заменил для наглядности на putchar() ).

Рабочий код проекта смотрите/забирайте отсюда. Там реализован пример из начала статьи.

Это стартовый вариант, для уверенного использования еще требуются некоторые доработки и тесты. Например, нужно предусмотреть механизм синхронизации при неблокирующем выводе - когда, скажем, вывод данных предыдущей функции еще не завершен, а мы в следующей функции уже переписываем буфер новой информацией. Также нужно еще внимательно поэкспериментровать с алгоритмами std::views. Например std::views::drop() при применении ее к строковому литералу или массиву char-ов, взрывается ошибкой "inconsistent directions for distance and bound". Ну что ж, стандарт новый, со временем освоим.

Как это работает можно посмотреть здесь. Проект поднят на двухядерном STM32H745; с одного ядра (480МГц) вывод идет в блокирующем режиме через отладочный интерфейс SWO, код примера выстреливается за 9,2 мкс, со второго(240МГц) - через Uart в режиме DMA, примерно за 20 мкс.

Как-то так.

Спасибо за внимание, буду рад отзывам и замечаниям, а также идеям и примерам, как это безобразие можно улучшить.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru