Вчера мое утро началось с того, что под Linux-ом компилятор GCC 13 отказался компилировать код вроде вот такого (это не реальный код, а минимизированая версия на которой проблема воспроизводится):
|
template< typename T, typename Range > [[nodiscard]] T make_from( const Range & r ) { return T{ r.begin(), r.end() }; } int main() { std::string src{ "Hello, World" }; auto v = make_from< values_container >( std::ranges::views::iota( 0u, src.size()) ); std::cout << v.front() << " - " << v.back() << std::endl; } |
Изначально код был написан под другую платформу с более свежим С++ным компилятором, поэтому там make_from был реализован иначе, проблем с компиляцией не было. А вот под уже довольно древним по современным меркам GCC возникли проблемы: компилятор не видел у values_container конструктора, который бы получал два итератора.
Конкретно в моем случае в качестве values_container был folly::fbvector, но это не суть. Вполне мог бы быть и std::vector, проблема была бы точно такой же.
Ошибка несколько обескуражила, т.к. я точно знал, что у values_container есть конструктор, принимающий итераторы first и last.
Но с этим-то удалось разобраться достаточно быстро: first и last должны быть одного типа. А в моем случае объект-рэндж, возвращенный вызовом iota, имел разные типы для begin() и для end(). Соответственно, раз типы итераторов разные, то и компилятор не может найти подходящий конструктор у values_container.
Ну OK, раз типы у итераторов разные, то можно к этой ситуации приспособить реализацию make_from. Например, написать ее так:
|
template< typename T, typename Range > [[nodiscard]] T make_from( const Range & r ) { T result; auto f = r.begin(); const auto l = r.end(); for( ; f != l; ++f ) result.push_back( *f ); return result; } |
Но как бы так не есть хорошо, потому что получается излишне пессимизированная версия, ведь мы теряем возможность более эффективной реализации когда типы итераторов совпадают.
Ну значит попробуем это учесть:
|
template< typename T, typename Range > [[nodiscard]] T make_from( const Range & r ) { using begin_it_t = std::decay_t< decltype(r.begin()) >; using end_it_t = std::decay_t< decltype(r.end()) >; if constexpr( std::same_as< begin_it_t, end_it_t > ) return T{ r.begin(), r.end() }; else { T result; auto f = r.begin(); const auto l = r.end(); for( ; f != l; ++f ) result.push_back( *f ); return result; } } |
Но и эта версия не самая эффективная, т.к. когда у нас типы итераторов не совпадают, то мы не имеем возможности сделать предварительный reserve. Или можем?
В принципе, можем. Есть такой концепт как sized_sentinel_for. С его помощью реализация принимает вид:
|
template< typename T, typename Range > [[nodiscard]] T make_from( const Range & r ) { using begin_it_t = std::decay_t< decltype(r.begin()) >; using end_it_t = std::decay_t< decltype(r.end()) >; if constexpr( std::same_as< begin_it_t, end_it_t > ) return T{ r.begin(), r.end() }; else { T result; auto f = r.begin(); const auto l = r.end(); if constexpr( std::sized_sentinel_for< end_it_t, begin_it_t > ) result.reserve( std::ranges::distance( f, l ) ); for( ; f != l; ++f ) result.push_back( *f ); return result; } } |
И вот когда новая версия make_from была получена меня посетил сильно запоздалый вопрос: а почему, собственно, iota возвращает объект-рэндж у которого begin() и end() имеют разные типы итераторов?
Ладно бы iota создавался для бесконечного интервала значений. Но ведь в моем случае есть фиксированный отрезок, так почему же для такого отрезка iota ведет себя подобным образом?
И вот тут "Зоркий глаз" наконец замечает, что в iota передаются значения двух разных типов: 0u -- это unsigned int, тогда как src.size() -- это std::size_t.
Может быть поэтому iota и делает для end() другой тип итератора, нежели для begin()?
ОК, заменяем вызов iota на вот такой:
|
auto v = make_from< values_container >( std::ranges::views::iota( std::size_t{ 0u }, src.size()) ); |
И, действительно, типы итераторов для begin() и end() теперь одинаковые.
Но код все равно не компилируется!
Причина в том, что begin() и end() теперь возвращают итераторы, у которых iterator_category определена как std::output_iterator_tag. Тогда как конструктор для вектора хочет итераторы категории std::input_iterator_tag (при этом folly::fbvector поддерживает еще и std::forward_iterator_tag).
Я оказался перед выбором: либо продолжать дорабатывать make_from, чтобы она могла работать и в случае с output-interators, либо же предложить переписать фрагмент с iota, дабы избежать всех этих наворотов внутри make_from.
Решил пойти по второму пути и начал описывать в текстовом документе собранные мной грабли для того, чтобы коллеги по проекту на них не наступали. А в процессе написания этого документа стал проверять разные варианты вызова iota. На разных компиляторах.
Оказалось, что вот такой вот вариант:
|
auto v = make_from< values_container >( std::ranges::views::iota( std::size_t{ 0u }, src.size()) ); |
не принимается GCC, но принимается VC++. Правда VC++ при этом выдает кучу предупреждений о неявном преобразовании от std::size_t к int.
И вот тут-то до меня дошло, что нужно глянуть на определение values_container. А values_container был вектором int-ов.
Не unsigned int, не std::size_t, а именно int. Но ведь объект-рэндж, возвращенный iota, производит не int-ы! В оригинале он давал unsigned int, в "исправленном" варианте -- std::size_t. Но не int.
Ладно, раз так, то меняем:
|
auto v = make_from< values_container >( std::ranges::views::iota( 0, static_cast<int>(src.size())) ); |
И все работает даже с первоначальной примитивной реализацией make_from:
|
#include <iostream> #include <vector> #include <ranges> #include <string> template< typename T, typename Range > [[nodiscard]] T make_from( const Range & r ) { return T{ r.begin(), r.end() }; } using values_container = std::vector<int>; int main() { std::string src{ "Hello, World" }; auto v = make_from< values_container >( std::ranges::views::iota( 0, static_cast<int>(src.size())) ); std::cout << v.front() << " - " << v.back() << std::endl; } |
Т.е. с int-ами у объекта-рэнджа и одинаковые типы итераторов для begin() и end(), и сами эти итераторы попадают в категорию input-iterators.
В итоге, вместо того, чтобы писать навороченную версию make_from, потребовалось всего лишь исправить вызов iota подставив туда правильные типы значений:
|
auto v = make_from< values_container >( std::ranges::views::iota( values_container::value_type{ 0 }, static_cast<values_container::value_type>(src.size())) ); |
PS. Если честно, то я не понимаю, почему в случае с std::size_t объект-рэндж использовал итераторы категории output-iterators. А разбираться с этим сейчас откровенно лень.
PPS. Если кому-то интересно, что за цирк происходит с категориями итераторов, то вот минималистичный пример с демонстрацией https://godbolt.org/z/cK3MKbTeT, а объяснение сего феномена есть на Stackoverflow.
Комментариев нет:
Отправить комментарий