Интервалы: грядущая эволюция C++

Уже скоро появится стандарт C++20, в который, скорее всего, добавят концепцию интервалов (ranges), однако мало кто знает, что они из себя представляют и с чем их едят. Доступных широкой аудитории русскоязычных источников про этого зверя мне найти не удалось, вследствие чего в данной статье я бы хотел подробнее про него рассказать, базируясь на лекции Arno Schödl «From Iterators to Ranges: The Upcoming Evolution Of the STL» с конференции Meeting C++ 2015-го года. Я постараюсь сделать эту статью максимально понятной для тех, кто впервые сталкивается с этим понятием, и одновременно расскажу про всевозможные фишки вроде интервальных адаптеров для тех, кто с этим понятием уже знаком и хочет узнать больше.

Библиотеки с ranges


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


Первая библиотека, по сути, прародитель данной концепции (что неудивительно, ведь чего только нет в собрании библиотек Boost :) ). Вторая — библиотека Эрика Ниблера (Eric Niebler), про неё будет рассказано позднее. И наконец, последняя библиотека, как нетрудно догадаться, написана компанией think-cell, которая, можно сказать, развила и усовершенствовала Boost.Range.

Почему интервалы это наше будущее?


Для тех, кто ещё не знаком с понятием интервала, определим это нетривиальное понятие как то, что имеет начало и конец (пара итераторов).

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

std::vector<T> vec=...;
std::sort( vec.begin(), vec.end() );
vec.erase( std::unique( vec.begin(), vec.end() ), vec.end() );

При этом мы указываем имя вектора аж 6 раз! Однако, используя концепцию интервалов (объединив итераторы на начало и конец вектора в один объект), можно написать в разы проще, указав искомый вектор лишь единожды:

tc::unique_inplace( tc::sort(vec) );

Что из интервалов есть на данный момент в рамках текущего стандарта?


В стандарте С++11 добавили range-based цикл for и универсальный доступ к началу/концу контейнеров, и в последнем стандарте С++17 ничего нового, связанного с интервалами, добавлено не было.

for ( int& i : <range_expression> ) {
 ...
}

std::begin/end(<range_expression>)

Будущее интервалов


Остановимся теперь на упомянутой ранее библиотеке Range V3. Эрик Ниблер, её создатель, в качестве своего домашнего проекта создал техническую спецификацию интервалов (Range's Technical Specification), модифицировав библиотеку algorithm для поддержки интервалов. Это выглядит примерно так:

namespace ranges {
    template< typename Rng, typename What > 
    decltype(auto) find( Rng && rng, What const& what ) {
        return std::find( 
            ranges::begin(rng),
            ranges::end(rng),
            what 
        );
    }
}

На его сайте есть некое превью того, что он хочет стандартизировать, это и есть Range V3.

Что же всё-таки может считаться range?


В первую очередь, контейнеры (vector, string, list etc.), ведь у них есть начало и конец. Ясно, что контейнеры владеют своими элементами, то есть, когда мы обращаемся к контейнерам, то мы обращаемся и ко всем их элементам. Аналогично при копировании и объявлении постоянной (глубокое копирование и константность). Во вторую очередь, views могут так же считаться интервалами. Views — это просто пара итераторов, указывающих соответственно на начало и конец. Вот их простейшая реализация:

template<typename It> 
struct iterator_range {
    It m_itBegin;
    It m_itEnd;
    It begin() const {
        return m_itBegin; 
    }
    It end() const { 
        return m_itEnd;
    } 
};

Views, в свою очередь, лишь ссылаются на элементы, поэтому копирование и константность ленивые (это не влияет на элементы).

Интервальные адаптеры


На этом изобретатели интервалов останавливаться не стали, ведь иначе эта концепция была бы довольно бесполезной. Поэтому ввели такое понятие, как интервальные адаптеры (range adaptors).

Трансформирующий адаптер


Рассмотрим следующую задачу: пусть дан вектор int'ов, в котором нужно найти первый элемент, равный 4:

std::vector<int> v;
auto it = ranges::find(v, 4);

Теперь же представим, что тип вектора не int, а какая-то сложная самописная структура, но в которой есть int, и задача всё та же:

struct A {
    int id;
    double data; 
};
std::vector<A> v={...};
auto it = ranges::find_if(
    v,
    [](A const& a) { return a.id == 4; }
);

Ясно, что эти два кода схожи по семантике, однако значительно различаются по синтаксису, ведь в последнем случае пришлось вручную писать функцию, пробегающую по полю int. Но если использовать трансформирующий адаптер (transform adaptor), то всё выглядит гораздо более лаконично:

struct A { 
    int id;
    double data; 
};
std::vector<A> v={...}; 
auto it = ranges::find(
    tc::transform(v, std::mem_fn(&A::id)), 
    4);

По сути, трансформирующий адаптор «трансформирует» нашу структуру, создавая вокруг поля int класс-обёртку. Ясно, что указатель указывает на поле id, но если бы мы хотели, чтобы он указывал на всю структуру, то необходимо дописать в конце .base(). Эта команда инкапсулирует поле, из-за чего указатель может пробегать по всей структуре:

auto it = ranges::find(
    tc::transform(v, std::mem_fn(&A::id)), 
    4).base();

Вот примерная реализация трансформирующего адаптера (он состоит из итераторов, каждый из которых имеет собственный функтор):

template<typename Base, typename Func> 
struct transform_range {
    struct iterator { 
    private:
        Func m_func; // в каждом итераторе
        decltype( tc::begin(std::declval<Base&>()) ) m_it; 
    public:
        decltype(auto) operator*() const { 
            return m_func(*m_it);
        }
        decltype(auto) base() const {
            return (m_it); 
        }
        ... 
    };
};

Фильтрующий адаптер


А если бы в прошлой задаче нам нужно было найти не первый такой элемент, а «профильтровать» всё поле int'ов на наличие таких элементов? В таком случае мы использовали бы фильтрующий адаптер (filter adaptor):

tc::filter( v,
    [](A const& a) { return 4 == a.id; } 
);

Заметим, что фильтр при этом выполняется лениво, во время итераций.

А вот его наивная реализация (примерно такая реализована в Boost.Range):

template<typename Base, typename Func> 
struct filter_range {
    struct iterator { 
    private:
        Func m_func; // функтор и ДВА итератора
        decltype( ranges::begin(std::declval<Base&>()) ) m_it;
        decltype( ranges::begin(std::declval<Base&>()) ) m_itEnd;
    public:
        iterator& operator++() {
            ++m_it;
            while( m_it != m_itEnd && !static_cast<bool>(m_func(*m_it)) )
                ++m_it; 
            return *this;
        }
        ... 
    };
};

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

Немного оптимизаций


Хорошо, а как выглядит итератор от tc::filter(tc::filter(tc::filter(...)))?

Boost.Range


В рамках реализации выше это выглядит так:

Слабонервным не смотреть!
m_func3
m_it3
m_func2
m_it2
m_func1
m_it1;
m_itEnd1;
m_itEnd2
m_func1
m_it1;
m_itEnd1;
m_itEnd3
m_func2
m_it2
m_func1
m_it1;
m_itEnd1;
m_itEnd2
m_func1
m_it1;
m_itEnd1;


Очевидно, это ужасно неэффективно.

Range V3


Давайте подумаем, как можно оптимизировать этот адаптер. Идея Эрика Ниблера состояла в том, что мы должны положить общую информацию (функтор и указатель на конец) в объект-адаптер, и тогда мы можем хранить ссылку на этот объект-адаптер и искомый итератор
*m_rng
m_it

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

Тык
m_rng3
m_it3
m_rng2
m_it2
m_rng1
m_it1


Это всё ещё не идеально, хотя и в разы быстрее предыдущей реализации.

think-cell, концепция индексов


Теперь рассмотрим решение компании think-cell. Они ввели так называемую концепцию индексов (index concept) для решения этой проблемы. Индекс — это такой итератор, который выполняет все те же операции, что и обычный итератор, но делает это, обращаясь к интервалам.

template<typename Base, typename Func>
struct index_range {
    ...
    using Index = ...;
    Index begin_index() const;
    Index end_index() const;
    void increment_index( Index& idx ) const; 
    void decrement_index( Index& idx ) const; 
    reference dereference( Index& idx ) const;
    ...
};

Покажем, как можно совместить индекс с обычным итератором.

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

template<typename IndexRng>
struct iterator_for_index {
    IndexRng* m_rng;
    typename IndexRng::Index m_idx;

    iterator& operator++() {
        m_rng.increment_index(m_idx); 
        return *this;
    }
    ... 
};

Тогда тройной фильтр будет реализован супер-эффективно:

template<typename Base, typename Func> 
struct filter_range {
    Func m_func; 
    Base& m_base;

    using Index = typename Base::Index;
    void increment_index( Index& idx ) const {
        do {
            m_base.increment_index(idx);
        } while ( idx != m_base.end_index() 
            && !m_func(m_base.dereference_index(idx)) );
    }
};

template<typename IndexRng>
struct iterator_for_index {
    IndexRng* m_rng;
    typename IndexRng::Index m_idx; 
    ...
};

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

Интервалы с lvalue и rvalue контейнерами


Теперь посмотрим, как работают интервалы с lvalue и rvalue контейнерами:

lvalue


Range V3 и think-cell ведут себя одинаково с lvalue. Предположим, что у нас есть такой код:

auto rng = view::filter(vec, pred1);
bool b = ranges::any_of(rng, pred2);

Тут у нас есть заранее объявленный вектор, который лежит в памяти (lvalue), и нам нужно создать интервал и потом как-то с ним работать. Мы создаём view с помощью view::filter или tc::filter и становимся счастливыми, никаких ошибок нет, и этот view мы потом можем использовать например, в any_of.

Range V3 и rvalue


Однако, если бы наш вектор ещё не был в памяти (например, если мы его только создавали), и перед нами стояла бы та же задача, то мы попробовали бы написать так и столкнулись с ошибкой:

auto rng = view::filter(create_vector(), pred1); // не скомпилируется
bool b = ranges::any_of(rng, pred2);

Почему же она возникла? View будет висячей ссылкой на rvalue из-за того, что мы создаём вектор и напрямую кладём в filter, то есть в фильтре будет rvalue ссылка, которая потом будет указывать на что-то неизвестное, когда компилятор перейдёт на следующую строку, и возникнет ошибка. Для того, чтобы решить эту проблему, в Range V3 придумали action:

auto rng = action::filter(create_vector(), pred1); // теперь скомпилируется
bool b = ranges::any_of(rng, pred2);

Action делает всё сразу, то есть просто берёт вектор, фильтрует по предикату и кладёт в интервал. Однако минус в том, что это больше не является ленивым, и think-cell постарались исправить этот минус.

think-cell и rvalue


В think-cell сделали так, что там вместо view создаётся контейнер:

auto rng = tc::filter(creates_vector(), pred1); 
bool b = ranges::any_of(rng, pred2);

В результате мы не сталкиваемся с подобной ошибкой, потому что в их реализации фильтр собирает rvalue контейнер вместо ссылки, поэтому это происходит лениво. В Range V3 так делать не захотели, потому что боялись, что будут ошибки из-за того, что фильтр ведёт себя то как view, то как контейнер, однако think-cell убеждены в том, что программисты понимают, как ведёт себя фильтр, а большая часть ошибок возникает именно из-за этой «ленивости».

Генераторные интервалы


Обобщим понятие интервалов. На самом деле, существуют и интервалы без итераторов. Они называются generator ranges (генераторные интервалы). Предположим, что у нас есть GUI виджет (элемент интерфейса), и мы вызываем виджет перемещения. У нас есть окно, которое просит переместить её виджет, также у нас есть кнопка в list box, и другое окно тоже должно листнуть свои виджеты, то есть мы вызываем traverse_widgets, который соединяет элементы в функтор (можно сказать, есть функция перечисления, где вы подключаете функтор, и функция перечисляет в этот функтор все элементы, которые у него есть).

template<typename Func>
void traverse_widgets( Func func ) {
    if (window1) {
        window1->traverse_widgets(std::ref(func));
    }
    func(button1);
    func(listbox1);
    if (window2) {
        window2->traverse_widgets(std::ref(func));
    }
}

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

mouse_hit_any_widget=tc::any_of(
    [] (auto func) { traverse_widgets(func); },
    [] (auto const& widget) {
        return widget.mouse_hit();
    }
);

think-cell стараются реализовывать методы так, чтобы они имели одинаковый интерфейс для всех видов интервалов:

namespace tc {
    template< typename Rng >
    bool any_of( Rng const& rng ) {
        bool bResult = false;
        tc::enumerate( rng, [&](bool_context b) {
            bResult = bResult || b;
        } );
        return bResult;
    }
}

Используя tc::enumerate, разница между интервалами скрывается, так как такая реализация придерживается концепции внутреннего итерирования (о том, в чём заключаются концепции external и internal iteration, рассказано подробнее на лекции), однако в такой реализации есть и свои минусы, а именно, std::any_of останавливается, как только встречается true. Такую проблему пытаются решать, например, добавляя исключения (так называемые прерываемые генераторные интервалы).

Заключение


Я ненавижу range-based цикл for, потому что он мотивирует людей писать его везде где нужно и где не нужно, из-за чего зачастую ухудшается лаконичность кода, например, люди пишут это:

bool b = false; 
for (int n : rng) {
    if ( is_prime(n) ) {
        b = true;
        break;
    }
}

вместо этого:

bool b = tc::any_of( rng, is_prime );
Поделиться публикацией

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

    0
    Спасибо за статью, тема ranges не частый гость на хабре.
    Не могли бы вы уточнить, что вам не нравится в коде, который упомянут в заключении? Да, ваш вариант короче, но при чём здесь эффективность? Алгоритмическая сложность одинаковая, особого оверхеда я тоже не вижу. В чём проблема?
      +2
      Более того, с учётом реализации any_of, предложенной выше, она ещё и медленнее. Она перебирает все элементы, а не останавливается на первом удовлетворяющем, как цикл. Так что зависит от реальных данных, а на общий случай нужны как минимум бенчмарки.
        0
        Вы правы, в данном случае эффективность даже хуже. Я имел в виду, что бывают ситуации, когда неопытные программисты пишут range-based for там, где без него можно обойтись, из-за его простого синтаксиса, вследствие чего помимо лаконичности может ухудшится и эффективность, хотя последнее происходит довольно редко. Извиняюсь за заблуждение.
          0
          Ну так неопытные напишут ещё и не то…
            +2

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

              0
              а можно, пожалуйста, пример, когда range-based for будет менее эффективен, чем альтернативы?
              0
              del
              +1

              Так уж и редкий гость. Самая горячая тема! Вот пара постов: исходник, и ответ на него.

              +7
              вспомнились слова песни «А козак чує — серденько мре.»
              На C++ не пишу, но с замиранием сердца читаю новости про расширение стандарта. Кто всё это сможет объять?
                +1
                Конкретно рэнджи — просто улучшения удобности и юзабельности стандартной библиотеки алгоритмов. Для конечного пользователя ничего сложного нет — просто меньше бойлерплейта и больше выразительности.

                Не думаю что причитания про необъятность стандарта уместны как минимум в этом вопросе.
                  +1
                  Сегодня — да, необъятность является приувеличением.
                  Однако темп, который взял комитет, заставляет задуматься, сколько абстракций будет в C++ через 10 лет.
                    +3
                    Наверное так просто кажется на фоне того застоя, который имел место быть в конце 90х и 00х. Другие языки тоже активно развиваются и обростают фичами, просто там частые и постоянные релизы, что дает ощущение бесшовности.
                  0
                  На C++ не пишу, но с замиранием сердца читаю новости про расширение стандарта. Кто всё это сможет объять?

                  ИИ написанный на java? ;)
                  0

                  Завершение статьи, конечно, фееричное:


                  Я ненавижу range-based цикл for, потому что он мотивирует людей писать его везде где нужно и где не нужно, из-за чего зачастую ухудшается лаконичность кода

                  Стандарт словно движется в сторону Java Streams или LINQ. А ценой лаконичности будет понижение производительности, когда неофиты набегут и будут совать это везде

                    +1
                    О, я не один такой в мире! Тоже пользую такой многословный цикл, ещё со времен басика в начале 90-х :). Зато наглядно.
                      +1

                      К нему кроме многословности ещё вопросы возникают.
                      ranged-for оперирует началом, концом и оператором ++.
                      Хочешь — запускай для вектора; хочешь — для списка или вообще собственного контейнера.
                      Но если контейнер, например, банальный вектор. И операция — удвоить всего его элементы — то за абстракцией ranged-for скрывается возможность поделить его на 8 частей и запустить операцию параллельно на 8 ядрах процессора. Потому что "8 частей" никак не вытащить из примитивов начало, конец и инкремент.

                      +5
                      А я не люблю range-based for за то, что он связывает синтаксис языка с определениями методов begin() и end(), которые являются по сути вторичными по отношению к синтаксису, но теперь попадают в некий раздел особых сущностей, как бы недоключевые слова. Это сломало красоту C++, в котором до этого не было таких особых случаев.

                      Тем не менее, я ими (циклами) пользуюсь. Всё таки сахар они, зараза, сладкий ) Даже синтаксический. Но, конечно, только там, где это не бьет по эффективности.
                        +2

                        Только в отличие от Java и C# всегда можно будет фоллбэкнуться до эффективной реализации, не меняя контекст языка. Когда этот цикл станет бутылочным горлышком — ну ты пойдешь и заоптимизируешь, а пока так.

                          0

                          Ну за Java не скажу, а в C# если отправной точкой считать LINQ (как это часто бывает), то фольекаться тоже есть куда.

                        0
                        Мне кажется, заголовок немного не соответствует теме. В стандарт вводятся именно range-v3, а большинство примеров на think-cell, что может немного вводить в заблуждение.
                          0
                          Не спорю, что в стандарт планируют ввести Range V3, однако у think-cell есть множество оптимизаций, поэтому их библиотека тоже стоит внимания.
                          +2
                          Вот серьезно, мне кажется в комитете стандарта С++ не хватает мужика с пулеметом, который бы расстрелял всех к чертям.
                          объединив итераторы на начало и конец вектора в один объект

                          А сам объект вектора не является тем самым объединением? Ей-богу, более уродливого синтаксиса и api чем в С++ найти невозможно…
                            +1

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

                              –1
                              почему это? вектор вполне себе может являтся интервалом, который включает в себя весь вектор. А вот если вам нужен интервал вектора со 2-го по 15-й элемент, то юзайте, пожалуйста, отдельный range.

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

                              но как таковая эта концепция не является чем-то новым в мире метапрограммирования, просто тема актуализировалась тем, что её хотят стандартизировать.
                                –1
                                хотелось быть первым, кто откомментит про range в python...))
                              0
                              Имелось в виду рассматривать итераторы на начало и конец непосредственно как одно целое, а не как два разных итератора (то есть создать view), но и объект вектора (как и все контейнеры) тоже считается интервалом, об этом я написал далее.
                              0
                              Range/Intervals — следствие дизайна STL им. математика Степанова, идея которой, как известно, пришла в делириуме после отравления рыбой.

                              Большинство техник на большинстве архитектур исторически базируется на применении адреса и счётчика, включая hardware. Например DMA или инструкции REP MOV (x86) или LDIR (z80). Я помню только одну подобную технику — её использовали процедуры «монитора» Радио-86РК и «Специалист». Они тоже копировали данные с указанием адреса первого и последнего байтов блока. Не адреса за блоком, как это делает STL, а именно последнего. Это может быть иногда удобно при ручной работе с командами копирования блоков памяти, но неудобно (и медленно) при непосредственном программировании.

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

                              При этом большинство библиотек/реализаций/high-level языков программирования предоставляет массивы/контейнеры хранящие свою длину. Для них не требуется указывать начало-конец контейнера, указывается только его имя (а также если необходимо — смещение и длина).

                              Так что это не «будущее», а очередная заплатка на неудобный/ошибочный/freaky дизайн. Многие заплатки Modern C++ — синтаксический сахарок, не привносящий actual value.
                                +5
                                Да! А ещё функциональный анализ не нужен для того, чтобы в магазине отсчитать нужное количество бутылок пива и расплатиться за это. Так что и его в топку его, Зина!
                                  +4
                                  пришла в делириуме после отравления рыбой

                                  это многое объясняет!
                                    0

                                    Не могу согласиться "в подавляющем большинстве". Если уж про то говорить, то в интернете в подавляющем большинстве находятся сайты, написанные поверх базы данных (готовой или самописной), а размер ответа запроса от той базы данных может не знать никто. Можно селектнуть из nosql базы итератор на ответ совершенно неясного размера, там может быть десять терабайт данных в хвосте запроса. Более того, сейчас уже нормально, что ответ этот может меняться, в той части, которую клиент ещё не видит, и с ним могут происходить разные другие трансформации. Для этого нужен крайне ленивый асинхронный API, такие операции поддерживающий.


                                    Мне кажется, тут есть некая большая проблема в видении языка. Разработчики стандарта думают о C++ как об универсальном языке для всего на свете — не менее универсальном, чем джава, сишарп или питон, но с другим набором стартовых предположений и трейдоффов. Но есть парочка узкоспециализированных категорий разрабов, например эмбеддед и геймдев, которые считают что C++ — их личный язык и никого другого они рядом не потерпят. И вот тут начинается великая битва :)

                                      0
                                      Мне кажется, тут есть некая большая проблема в видении языка. Разработчики стандарта думают о C++ как об универсальном языке для всего на свете — не менее универсальном, чем джава, сишарп или питон

                                      «строго более» универсальном. Всё-таки с++ применим еще и там, где интерпретаторы/вм либо не работают, либо работают недостаточно быстро.
                                      0
                                      Сама семантика структур данных естественного мира

                                      Что за структуры данных в естественном мире?


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

                                      А толку с этого количества? Вам для этого произвольный доступ нужен, а структур данных, поддерживающих произвольный доступ по индексу, не так много.


                                      Так что это не «будущее», а очередная заплатка на неудобный/ошибочный/freaky дизайн.

                                      Если достаточно упарываться функциональщиной, то становится понятно, что функциональные ленивые списки (и плюсовые range'и) — это управляющая структура данных, а не контейнер. Там ещё, правда, корутины не помешают, чтобы работать с этим всем действительно удобно, а не через адовое нагромождение рейнджей, но то такое.


                                      Ну и вообще вот.

                                      0
                                      del
                                        +3
                                        Но если использовать трансформирующий адаптер (transform adaptor), то всё выглядит гораздо более лаконично:

                                        Да как-то не увидел я там лаконизма: примерно одинакового размера фрагменты. Меня как-то вот эти фильтры и адаптеры не радуют. Да, я прекрасно помню, как использовать bind(), men_fn() и тому подобное, но в эпоху до C++11 выбор обычно был между «собери предикат из готовых кусков» и «пиши отдельную функцию/функциональный объект». Когда появились лямбды, стало возможно прямо на месте чётко выразить, чего хочется. А теперь вы предлагаете опять собирать простой и ясно выглядящий критерий вида «a.id == 4» из каких-то кусков, которые читающему ещё расшифровывать надо. Попробуйте заменить мысленно выражение под оператором if() таким вот кодом. Никто же так не пишет, и неудивительно.
                                          0
                                          допустим, у вас есть алгоритм, состоящий из 3-7 «кирпичиков». Когда код написан на циклах/if'ах (по 1-2 на алгоритм), читающему сначала придется разобраться, что делает каждый из этих циклов. Назначение алгоритмов же понятно по их общеизвестным именам. Сейчас у кода с алгоритмами есть синтаксический оверхед из-за итераторов и лямбд, но в ranges итераторы нужны намного реже.

                                          Из моего опыта, код на нынешних алгоритмах в среднем значительно короче. Просто это плохо заметно на совсем тривиальных примерах, как в статье.
                                            0
                                            Да, понимаю. Но я и не ратую за циклы и if-ы, мне не нравится попытка заменить лямбды предикатами, склеенными из стандартных функций. Вот эта штука плохо читается.

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

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