Примеры C++ кода до и после Ranges

Автор оригинала: Marius Bancila
  • Перевод
Снова здравствуйте. Перевод следующего материала подготовлен специально для студентов курса «Разработчик C++», занятия по которому стартуют уже 27 июня.



Библиотека Ranges была принята в C++20 на совещании стандартного комитета в Сан-Диего в ноябре прошлого года. Библиотека предоставляет компоненты для обработки диапазонов значений, направленных на упрощение нашего кода. К сожалению, библиотека Ranges не очень хорошо документирована, из-за этого ее труднее понять тем, кто хотел бы ее освоить. Этот пост предназначен для ознакомления с примерами кода, написанного с использованием Ranges и без нее.

Реализация библиотеки Ranges Эрика Ниблера доступна здесь. Она работает с Clang 3.6.2 или новее, gcc 5.2 или новее, и VC ++ 15.9 или новее. Примеры кода ниже были написаны и протестированы с последними версиями компиляторов. Стоит отметить, что эти примеры представляют собой типичные реализации и не обязательно являются единственными решениями, которые можно придумать.

Хотя стандартным пространством имен для библиотеки Ranges является std::ranges, в данной текущей реализации библиотеки оно ranges::v3.

Следующие псевдонимы пространства имен используются в примерах ниже:

namespace rs = ranges::v3;
namespace rv = ranges::v3::view;
namespace ra = ranges::v3::action;

Также, для упрощения, мы будем ссылаться на следующие объекты, функции и лямбды:

std::string to_roman(int value)
{
   std::vector<std::pair<int, char const*>> roman
   {
      { 1000, "M" },{ 900, "CM" },
      { 500, "D" },{ 400, "CD" },
      { 100, "C" },{ 90, "XC" },
      { 50, "L" },{ 40, "XL" },
      { 10, "X" },{ 9, "IX" },
      { 5, "V" },{ 4, "IV" },
      { 1, "I" }
   };
 
   std::string result;
   for (auto const & [d, r]: roman)
   {
      while (value >= d)
      {
     	result += r;
     	value -= d;
      }
   }
 
   return result;
}
 
std::vector<int> v{1,1,2,3,5,8,13,21,34};
 
auto print_elem = [](auto const e) {std::cout << e << '\n'; };
 
auto is_even = [](auto const i) {return i % 2 == 0; };

АПДЕЙТ: Я хотел бы поблагодарить Эрика Ниблера и всех остальных, кто комментировал ниже, с предложениями для этих примеров кода. Я обновил несколько на основе их отзывов.

Вывести все элементы диапазона:

До Ranges После Ranges
С++ С++
std::for_each(
   std::cbegin(v), std::cend(v),
   print_elem);
 
// or
  
for(auto const i : v)
{
   print_elem(i);
};
rs::for_each(
   std::cbegin(v), std::cend(v),
   print_elem);
 
// or
 
rs::for_each(std::as_const(v), print_elem);



Выведите все элементы диапазона в обратном порядке:

До Ranges После Ranges
С++ С++
std::for_each(
   std::crbegin(v), std::crend(v),
   print_elem);
 
rs::for_each(
   std::crbegin(v), std::crend(v),
   print_elem);
 
// or
 
for (auto const i : v | rv::reverse)
{
   print_elem(i);
};


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

До Ranges После Ranges
С++ С++
std::for_each(
   std::crbegin(v), std::crend(v),
   [print_elem](auto const i) {
      if(i % 2 == 0)
     	print_elem(i);
   });
 
for (auto const i : v
                  | rv::reverse
                  | rv::filter(is_even))
{
   print_elem(i);
};


Пропустите первые два элемента диапазона и выведите только четные из следующих трех:

До Ranges После Ranges
С++ С++
 
auto it = std::cbegin(v);
std::advance(it, 2);
auto ix = 0;
while (it != std::cend(v) && ix++ < 3)
{
   if (is_even(*it))
      print_elem(*it);
   it++;
}
 
for (auto const i : v
                  | rv::drop(2)
                  | rv::take(3)
                  | rv::filter(is_even))
{
   print_elem(i);
};


Выведите числа от 101 до 200:

До Ranges После Ranges
С++ С++
for (int n = 101; n <= 200; ++n)
{
   print_elem(n);
}
 
for (auto n : rs::iota_view(101, 201))
{
   print_elem(n);
}


Выведите все римские цифры от 101 до 200. Чтобы преобразовать число в соответствующее римское число, используется функция to_roman(), показанная выше.

До Ranges После Ranges
С++ С++
 
for (int i = 101; i <= 200; ++i)
{
   print_elem(to_roman(i));
}
 
for (auto n : rs::iota_view(101, 201)
            | rv::transform(to_roman))
{
   print_elem(n);
}
 
// or
 
rs::for_each(rv::iota(101, 201),
         	print_element, to_roman);


Выведите римские цифры последних трех чисел, делимых на 7 в диапазоне [101, 200], в обратном порядке.

До Ranges После Ranges
С++ С++
 
for (int n = 200, count=0; n >= 101 && count < 3; --n)
{
   if (n % 7 == 0)
   {
      print_elem(to_roman(n));
      count++;
   }
}
 
for (auto n : rs::iota_view(101, 201)
            | rv::reverse
            | rv::filter([](auto v) {
                return v % 7 == 0; })
            | rv::transform(to_roman)
            | rv::take(3))
{
   print_elem(n);
}


Создайте диапазон строк, содержащий римские цифры последних трех чисел, кратных 7 в диапазоне [101, 200], в обратном порядке.

До Ranges После Ranges
С++ С++
 
std::vector<std::string> v;
for (int n = 200, count = 0;
 	n >= 101 && count < 3; --n)
{
   if (n % 7 == 0)
   {
      v.push_back(to_roman(n));
      count++;
   }
}
auto v = rs::iota_view(101, 201)
   	| rv::reverse
   	| rv::filter([](auto v) {return v % 7 == 0; })
   	| rv::transform(to_roman)
   	| rv::take(3)
   	| rs::to_vector;


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

До Ranges После Ranges
С++ С++
 
std::vector<int> v{ 21, 1, 3, 8, 13, 1, 5, 2 };
 
std::sort(std::begin(v), std::end(v));
v.erase(
   std::unique(std::begin(v), std::end(v)),
   std::end(v));
std::reverse(std::begin(v), std::end(v));
 
std::vector<int> v{ 21, 1, 3, 8, 13, 1, 5, 2 };
 
v = std::move(v) |
    ra::sort |
    ra::unique |
    ra::reverse;


Удалите два наименьших и два самых больших значения диапазона и оставьте остальные, упорядоченные во втором диапазоне.

До Ranges После Ranges
С++ С++
 
std::vector<int> v{ 21, 1, 3, 8, 13, 1, 5, 2 };
std::vector<int> v2 = v;
std::sort(std::begin(v2), std::end(v2));
      
auto first = std::begin(v2);
std::advance(first, 2);
auto last = first;
std::advance(last, std::size(v2) - 4);
 
v2.erase(last, std::end(v2));
v2.erase(std::begin(v2), first);
 
std::vector<int> v{ 21, 1, 3, 8, 13, 1, 5, 2 };
auto v2 = v |
          rs::copy |
          ra::sort |
          ra::slice(2, rs::end - 2);


Объединить все строки в данном диапазоне в одно значение.

До Ranges После Ranges
С++ С++
 
std::vector<std::string> words {
   "Lorem", " ", "ipsum", " ",
   "dolor", " ", "sit", " ",
   "amet"};
 
std::string text;
for (auto const & word : words)
   text += word;
 
std::vector<std::string> words {
   "Lorem", " ", "ipsum", " ",
   "dolor", " ", "sit", " ",
   "amet"};
 
std::string text = words |
               	rs::move |
               	ra::join;


Подсчитайте количество слов (разделенных пробелом) в тексте.

До Ranges После Ranges
С++ С++

auto text = "Lorem ipsum dolor sit amet";
 
std::istringstream iss(text);
std::vector<std::string> words(
   std::istream_iterator<std::string>{iss},
   std::istream_iterator<std::string>());
auto count = words.size();
 
 
// or
 
size_t count = 0;
std::vector<std::string> words;
std::string token;
std::istringstream tokenStream(text);
while (std::getline(tokenStream, token, ' '))
{
   ++count;
}

auto text = "Lorem ipsum dolor sit amet";
 
auto count = rs::distance(
   rv::c_str(text) | rv::split(' '));


Была ли статья полезной для вас? Пишите в комментарии.
OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 49

    +14

    1) Не хватает в сравнении хоть каких-то замеров по скорости работы и скорости компиляции. Причем особенно интересует режим отладки.


    2) Как себя ведут рэнджи относительно исключений? Бросают ли сами? как реагируют на то, если итератор бросит?


    3) Хотелось бы еще пример того, что будет показывать компилятор при опечатке и имени ренджовой функции. Боюсь что-то очень страшное.


    4) Работают ли в constexpr контексте?

      +8
      Причем особенно интересует режим отладки.

      Ну чего вы, нормально же общались.


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

        +2
        Современные плюсы очень полагаются на оптимизатор.

        современные плюсы (если говорить именно о коде стандартной библиотеки) сильно полагаются на одну оптимизацию — инлайнинг. Которую для полноценной отладки необходимо отключать. Концепты могут упростить код стандартной библиотеки, особенно в местах, где используется много sfinae, прокси-методов и сложные иерархии наследования. В ranges, однако, достаточно сильная вложенность by design. Но то, что они плохо подходят для отладочных билдов, не значит, что их не стоило принимать.

        На мой взгляд, компиляторам просто нужен флажок (по умолчанию) для инлайнинга функций/методов из стандартной библиотеки, даже в дебаг режиме. Ведь с практической точки зрения, баг практически всегда не в них
          0
          Ну pragma optimize в MSVC есть, и он худо-бедо-костыльно эту проблему решает. Я для кода, который требовал тяжелых хидеров, для дебага делал так — для дебаг конфигурации форсился флаг /O2, а после подключения всех заголовков, для своего уже кода вставлялась pragma. Да, многословно и костыльно, но по итогу делало то что нужно.
            +1
            современные плюсы (если говорить именно о коде стандартной библиотеки) сильно полагаются на одну оптимизацию — инлайнинг.

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


            Которую для полноценной отладки необходимо отключать.

            Ну фиг знает, я за всю практику припомню, наверное, один или два случая, когда инлайнинг действительно мешал отладке, да и в том случае я отлаживал чужой код.


            Конечно, инлайнинг не даст вам полноценный стек (а последующие оптимизации не дадут значения переменных, будет одно сплошное optimized out), но конструктивно отлаживаться всё равно можно. По моему опыту, опять же, по крайней мере.


            На мой взгляд, компиляторам просто нужен флажок (по умолчанию) для инлайнинга функций/методов из стандартной библиотеки, даже в дебаг режиме.

            А лямбды, которые вы туда передаёте, инлайнить и иначе оптимизировать надо?

              0
              Конечно, инлайнинг не даст вам полноценный стек (а последующие оптимизации не дадут значения переменных, будет одно сплошное optimized out), но конструктивно отлаживаться всё равно можно. По моему опыту, опять же, по крайней мере.

              ну это сильно зависит от того что вы понимаете под «полноценной отладкой», о которой я вел речь. Иногда для локализации бага достаточно знать имя функции с ошибкой. А иногда хочется погулять по брейкпоинтам, отследить значения переменных, и т.д. Хотя стек вызовов может быть сильно зашумлен вложенными функциями.

              А лямбды, которые вы туда передаёте, инлайнить и иначе оптимизировать надо?

              Пользовательские лямбды — нет, очевидно.
            0
            А если серьезно, есть впечатление, что комитет давно забил на скорость работы в сборках без оптимизаций.

            С++ до «забил» ещё очень далеко. Недавно кто-то пытался дождаться выполнения дебажной сборки раста — не дождался.

            сборках без оптимизаций.

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

            Отладка подобной сборки не всегда возможна и зачастую попросту вредна. Не все программы могут работать с подобных замедлением + многопоточна среда(и не обязательно эти потоки уровня ОС).

            Почему вредна? Всё очень просто. Опять же, многопоточная среда. O0 по-сути предполагает volatile для всех переменных. Т.е. поведение разных сборок будет различным.

            В ситуации же с более мощной и универсальной отладкой трейсом — таких проблем нет. Как и нет проблем с отладкой подобных пайпов. Без проблем пишется операция «трейс» и вставляет куда угодно в пайп. А трейс — это просто подвид тестирования.

            Из этого следует то, что подобные решения(даже если они имеют упомянутые вами проблемы) не приводят к проблемам отладки. Они приводят к проблемам лишь в одной методике отладки. А проблемы одного из подходов не являются проблемой в целом, а являются проблемой локальной для подхода.

              0
              Недавно кто-то пытался дождаться выполнения дебажной сборки раста — не дождался.

              Где почитать?

                +1
                Где почитать?

                Везде. Тема поднималась тысячи раз. Я ссылался на это Где почитать — не знаю, меня эта тема не интересует.

                  0

                  Тьфу, я уж думал правда сам раст в дебаге собрать не могут.

                0
                Мотивация там не столько вырубить оптимизатор, а сколько обеспечить прямое соответствие написанному и сгенерированному коду.

                Ну, опять же, это, на мой сугубо личный и эмпирический взгляд, не всегда настолько уж полезно, как я писал в соседнем комментарии. -O2 -g вполне достаточно.


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


                В ситуации же с более мощной и универсальной отладкой трейсом — таких проблем нет.

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


                Ошибки выражения логики на языке лучше отлаживать asan'ом, tsan'ом и тому подобным. Ошибки самой логики — тесты там всякие.

                  0
                  Ну, опять же, это, на мой сугубо личный и эмпирический взгляд, не всегда настолько уж полезно, как я писал в соседнем комментарии. -O2 -g вполне достаточно.

                  Это не моя мотивация и я ничего не знаю об этом. Для меня так же все минусы -O0 перекрывают плюсы.

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

                  Смотря как написать трейс.

                  Ошибки выражения логики на языке лучше отлаживать asan'ом, tsan'ом и тому подобным. Ошибки самой логики — тесты там всякие.

                  Трейс является источником данных. Интерпретировать их может не только человек, а интерпретация человек уже по-сути тестирование.

                    0
                    Смотря как написать трейс.

                    Даже как memcpy в заmmapленный файл.

                      0
                      Даже как memcpy в заmmapленный файл.

                      Трупут memcpy десятки гагабайт на ведро. Трупут современной файловой подсистемы не далеко от этого ушел. 99% программ даже сотен мегабайт трейса не генерируют. Такое кол-во программ даже близко не являются оптимальными, причём эта неоптимальность не исчисляется процентами, а исчисляется разами.

                      И что самое интересное, эти рассуждения вообще не имеют смысла. Если для отладки чего-то необходим даже мегабайт трейса — отладить это вручную попросту невозможно. Здесь нет какого-либо выбора «трейс или не трейс» и какие-то претензии к трейсу попросту нестоятельны.

              0
              Не хватает в сравнении хоть каких-то замеров по скорости работы

              Замерять нужно уже реализации из stdlib. Но в целом там нечему тормозить. Лично я разницы не заметил, но мой опыт ограничен прототипированием. И то недолгим т.к. они тогда собирались крайне медленно.

              скорости компиляции

              Текущая реализация очень тормозная, в том числе и по причине эмуляции концептов. Но реализация и не претендует на оптимальность.

              2) Как себя ведут рэнджи относительно исключений? Бросают ли сами? как реагируют на то, если итератор бросит?

              А что именно они там бросать должны и на что реагировать? Реагируют так же как и какой-нибудь std::transform()|range based for.

              3) Хотелось бы еще пример того, что будет показывать компилятор при опечатке и имени ренджовой функции. Боюсь что-то очень страшное.


              main.cpp:14:51: ошибка: «splite» is not a member of «rv»; did you mean «split»?
                 14 |   auto count = rs::distance(rv::c_str(text) | rv::splite(' '));
                    |                                                   ^~~~~~
                    |                                                   split


              main.cpp:17:62: ошибка: no match for «operator|» (operand types are «ranges::split_view<ranges::delimit_view<ranges::subrange<const char*, ranges::unreachable_sentinel_t, ranges::subrange_kind::unsized>, char>, ranges::single_view<char> >» and «int»)
                 17 |   auto count = rs::distance(rv::c_str(text) | rv::split(' ') | x);
                    |                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^ ~
                    |                                             |                  |
                    |                                             |                  int
                    |                                             ranges::split_view<ranges::delimit_view<ranges::subrange<const char*, ranges::unreachable_sentinel_t, ranges::subrange_kind::unsized>, char>, ranges::single_view<char> >
              


              Вот пример: godbolt.org/z/VSykII

              4) Работают ли в constexpr контексте?

              Смотря что. Много работает, а много нет. В стандарте, думаю, будет работать всё.

              +4
              Удалите два наименьших и два самых больших значения диапазона и оставьте остальные, упорядоченные во втором диапазоне.

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

              Выведите числа от 101 до 200:

              не уверен что такую задачу решать через range лучше

              Объединить все строки в данном диапазоне в одно значение.

              лишний rs::move справа

              Подсчитайте количество слов (разделенных пробелом) в тексте.

              если не обрабатывать ошибки, то проще вернуть std::count(..., ' ') + 1. А вообще используйте isspace, хорошая штука
                0
                Удалите два наименьших и два самых больших значения диапазона и оставьте остальные, упорядоченные во втором диапазоне.
                материализовать рендж забыли, не эквивалентный код получается
                Ещё одна проблема этого кода — несоответствие спецификации. Удаляется по 2 элемента с начала и с конца. Но даже в тестовых данных, после сортировки нужно удалить 3 элемента с начала, т.к. элементы не уникальные (единица повторяется).
                +12
                Все, сдаюсь! Больше не буду писать в резюме знание C/C++.
                  +3

                  Да наоборот же проще стало, раньше чтобы посчитать количество слов в файле на гигабайт надо было как-то напрягаться, писать что-то, а теперь будет что-нибудь типа (rf::file(Name) | rs::to_string | rv::split).size().
                  Хотя в реальности конечно это очередной новый стандарт, который заменит все предыдущие (нет), а вас будет ждать еще больше сюрпризов при работе с будущими легаси-проектами, в которых будут еще и ranges в самых неожиданных местах и кейсах использования, ибо фантазия людская безгранична, и ознакомившись с теми же ренжами некоторые будут делать через них вообще всё, даже если на голом си это было бы в 5 раз короче.

                    –1

                    Думаю С можете оставить :)

                      0
                      del (промазал)
                      0

                      Пора на пенсию...

                        0
                          0
                          Эмм, не правильно прочитал, написал, потом пересмотрел код и понял, можно ли как-то комментарий удалить?
                            0
                            Клёво. Мне подобной библиотеки в C++ не хватало.
                            А MooNDeaR хорошие вопросы задаёт. Кто нибудь может прокомментировать, как обстоят дела с исключениями?
                              0
                              А чего именно из boost::adaptors вам не хватало?
                              0
                              Уффффф…
                                +3
                                Верно ли, что вычисления будут ленивыми, что сначала не вычислится весь reverse в какую-нибудь временную переменную, потом все это не отфильтруется еще куда-нибудь, только чтобы взять 3 элемента с соответствующим расходом времени и памяти?
                                Верно ли, что после take(3) вычисление reverse прекратится, как и весь алгоритм?
                                auto v = rs::iota_view(101, 201)
                                   	| rv::reverse
                                   	| rv::filter([](auto v) {return v % 7 == 0; })
                                   	| rv::take(3);
                                
                                  +2

                                  Да, views как раз представляют такие ленивые диапазоны.

                                  +4
                                  Похоже ребята из комитета в школе увлекались перлом/бреинфаком. Или просто ненавидят всех инженеров на планете. В любом случае не завидую тому кто такие поделки будет дебажить.
                                    0

                                    Что поделать…
                                    C++ имеет возможность переопределения операторов, а у ребят из комитета есть жгучее желание эту возможность использовать в стиле "ыыы, смотрите как можно".


                                    Лично я бы допилил uniform function call syntax и сделал бы без извращенств с операторами. Вместо


                                    for (auto const i : v | rv::drop(2) | rv::take(3) | rv::filter(is_even))

                                    хочу видеть так:


                                    for (auto const i : v.drop(2).take(3).filter(is_even))
                                      0
                                      C++ имеет возможность переопределения операторов, а у ребят из комитета есть жгучее желание эту возможность использовать

                                      И в чём же проблема? Какая разница как это сделано? Оператором или каким-нибудь кейвордом/конструкциями?
                                      в стиле «ыыы, смотрите как можно».

                                      В стиле «ыыы, мы можем использовать возможности языка и делать нормальные api»? Как это глупо, как это странно.

                                      Лично я бы допилил uniform function call syntax

                                      Это такая же перегрузка не очевидная. В чём разница? Да и что будем делать с коллизиями имён?

                                      без извращенств с операторами

                                      В чём «извращенств» заключается?

                                      хочу видеть так:

                                      У этого подхода все аналогичные проблемы(хотя я наличия проблем не вижу ни там, ни там). Допустим, если я хочу сохранить операцию — можно сделать auto op = filter(is_even); Если же я захочу сделать это с функций — мне нужно описывать функцию с достаточно не очевидным параметром. Какой концепт мы напишем у op? Какой он у filter? Если мы просто напишем auto — мы получим плохую ошибку. Нам нужно будет писать какой-нибудь кастомный bind.

                                        0
                                        И в чём же проблема? Какая разница как это сделано? Оператором или каким-нибудь кейвордом/конструкциями? В чём «извращенств» заключается?

                                        Мне неприятно, когда операторы имеют неочевидную семантику.
                                        operator | — в большинстве языков это бинарный оператор, побитовый OR. Навешивание на него другого функционала может затруднить чтение кода.


                                        Это такая же перегрузка не очевидная. В чём разница?

                                        Разница в том, что не переопределяется оператор побитового или.


                                        Да и что будем делать с коллизиями имён?

                                        То же самое, что и в случае операторов.
                                        Кстати, UFCS хорош тем, что в библиотечном шаблонном коде можно писать rv::drop(v, 2), и никакой коллизии не будет.


                                        Допустим, если я хочу сохранить операцию — можно сделать auto op = filter(is_even); Если же я захочу сделать это с функций — мне нужно описывать функцию с достаточно не очевидным параметром.

                                        auto op = [](auto v) -> { return v.filter(is_even); }

                                        Вообще, я довольно много писал на C# с использованием LINQ (а там это сделано через расширения), ни разу не приходилось фильтр сохранять.

                                        0
                                        Не, первый вариант красивее, с.точками.всё.сливается.и.вообще.ничего.не.понятно
                                      0
                                      Зачем все так усложнять? было эпиграфом статьи. Я бы добавил во все примеры еще один столбец, написанном на С. Желающие могут сами попробовать написать, и задать себе этот вопрос.

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

                                      Самое читаемое