Возьмите std::sort и std::ranges::sort в качестве примера, класс итератора в std::ranges::sort
ограничен концепцией std::random_access_iterator
:
template< std::random_access_iterator I, std::sentinel_for<I> S,
class Comp = ranges::less, class Proj = std::identity >
requires std::sortable<I, Comp, Proj>
constexpr I
sort( I first, S last, Comp comp = {}, Proj proj = {} );
Но std::sort
это не:
template< class RandomIt >
void sort( RandomIt first, RandomIt last );
Почему std::sort
(и все алгоритмы без ранжирования) не ограничены?
Связанный с этим вопрос: В чем разница между std::fill_n и std::ranges::fill_n?
ИМХО: C++ — это плата за то, что вы используете язык. Если вам нужны ограничения типов и концепции, платите за это, используя диапазоны. Если нет, продолжайте использовать <algorithm>
и не платите за дополнительное время компиляции, выполняющее ограничения.
C++20 представил как std::ranges
, так и концепции, поэтому дал очевидную возможность/мотивацию для проектирования/спецификации std::ranges
для использования концепций. Старые алгоритмы, которые принимают пары итераторов (например, std::sort()
), появились раньше концепций и просто не обновлялись в C++20. Это типично для того, как развивается стандарт C++ (и большинство стандартов с формальным процессом управления) — требуется время/усилия для обновления старых функций для использования новых функций, и это увеличивает риск поломки чего-либо. Будущие предложения (в зависимости от времени/усилий) могут изменить или не изменить это.
@NathanOliver, но была ли когда-либо «плата за то, что вы используете» применима ко времени компиляции? Если ограниченный std::sort
для допустимых входных данных будет создавать тот же двоичный код, что и нынешняя версия без ограничений, почему я должен беспокоиться об увеличении времени компиляции? Возможно, причиной отсутствия ограничений старых алгоритмов является обратная совместимость, и в этом случае, я полагаю, некоторые ограничения могут сломаться во время компиляции кода, который был допустим в старых стандартах, а некоторые могут сломать код, который изначально был недопустим, что и произошло. работать только из-за УБ.
Я полагаю, что в принципе можно добавить ограничения, которые могут привести к поломке последнего типа.
Конечно, есть практические и исторические причины; но если кто-то хочет подумать о причине постфактум:
В противном случае вы сломаете много кода; Требования к стилю диапазона более ограничены, чем вы можете себе представить.
Существует много кода (пользовательских итераторов), который почти случайно работает с алгоритмами и определенно не компилируется с std::ranges
алгоритмами.
Возьмем, к примеру, общее требование в std::ranges
, такое как indirectly_writable
https://en.cppreference.com/w/cpp/iterator/indirectly_writable.
Могу поспорить, что почти все нетривиальные итераторы, написанные до Concepts, на самом деле не будут соответствовать всем наложенным ограничениям.
Интересно, действительно ли многие из случаев «почти случайно сработавших» на самом деле являются UB?
Для сравнения диапазонов требуется «весь» набор сравнения, тогда как старые алгоритмы требуют только <
и/или ==
. Использование полных концепций приведет к поломке кода, а предоставление неполных концепций кажется плохим.
@Enlico, это не UB в обычном смысле времени выполнения. Это на более смысловом уровне. Как сказал Джарод, предположим, что тип определяет op< и op==. Для классического STL этого достаточно, чтобы тип был заказан. вы даже можете оставить неопределенными op> и op!= или даже определить их независимым (непоследовательным) способом. STL claasic не заметит. Однако диапазоны STL могут жаловаться на противоречивые или неопределенные операции, даже если вы знаете, что реализация их не использует!
@alfC, поэтому я сказал «многие», а не все. Случай, о котором вы говорите, был законным кодом, и он остается таковым, потому что старые алгоритмы не были ограничены. Если бы старые алгоритмы были ограничены, этот код сломался бы во время компиляции. Интересно, существует ли закат ограничений, которые, если их добавить, сломают только (во время компиляции и/или времени выполнения?) код, который изначально демонстрировал UB. Может быть, я просто говорю чушь.
Нет, я думаю, что ваше беспокойство обосновано. и да, может быть UB, но это больше похоже на «библиотечный» UB в том смысле, что пользовательские реализации итераторов могут работать с некоторыми реализациями STL, а не с другими, или что небольшие изменения в реализации могут привести к тому, что некоторые вещи не компилируются, учитывая разные или неправильные результаты или, что маловероятно, но возможно, UB, как вы сказали. UB всегда возможен, я говорю о том, что это не является чем-то особенным, связанным с наложением ограничений.
Это «ограничено», но не в концептуальном смысле:
Если параметр шаблона алгоритма имеет имя
RandomAccessIterator
,RandomAccessIterator1
илиRandomAccessIterator2
, аргумент шаблона должен соответствовать требованиям Cpp17RandomAccessIterator ([random.access.iterators] ), если он должен быть изменяемым итератором или модельюrandom_access_iterator
( [iterator.concept.random.access]) иначе.
Это требование «должно» можно проверить с помощью static_assert
в вашей стандартной библиотеке C++ (и это всегда было возможно). Это просто проблема качества реализации, если разработчики стандартной библиотеки ищут вещи, которые на самом деле не используются.
Это отличается от концептуально ограниченного параметра, где функция не участвует в разрешении перегрузки.
template<typename T>
concept is_std_sortable = requires(T t) { std::sort(t, t); };
template<typename T>
concept is_std_ranges_sortable = requires(T t) { std::ranges::sort(t, t); };
static_assert(is_std_sortable<int>);
static_assert(!is_std_ranges_sortable<int>);
Вы можете увидеть, насколько это необходимо, с помощью нескольких перегрузок (диапазон или пара итераторов) и аргументов по умолчанию для выбора правильной функции при вызове.
В этом нет необходимости для алгоритмов без диапазонов, которые имеют только одну перегрузку. Те, которые имеют несколько перегрузок, принимают политику выполнения в качестве первого аргумента, а вторая перегрузка по сути ограничена концепцией ([algorithms.parallel.overloads]p4).
Может быть, глупый вопрос: зачем это нужно при многократных перегрузках? Что делать, необходимо?
@Enlico Если бы не было ограничений, то std::ranges::sort(vec.begin(), vec.end())
было бы неоднозначно: перегрузка пары итераторов или перегрузка диапазона, где вторым аргументом был компаратор. Аналогичная проблема возникла с конструктором пары итераторов std::string
, конкурирующим с перегрузкой size_type, CharT
(№10 здесь: en.cppreference.com/w/cpp/string/basic_string/basic_string).
Думаю, я понял исходную мысль, но не совсем уловил аналогию, потому что те две перегрузки, о которых вы упоминаете std::string
, не являются двусмысленными, не так ли?
Это правильно, но этот ответ больше касается внутренней механики std::range
, чем того, почему алгоритмы без диапазонов не ограничены требованиями.
@Энлико char count = 12; std::string(count, 'c'); // Would have called the two iterator overload
. Или int count = 12; int char = 'c'; std::string(count, char);
@alfC Три момента: алгоритмы без диапазонов имеют «ограничения», алгоритмы без диапазонов не нуждаются в ограничениях, таких как std::ranges::
, в ситуациях, когда алгоритмам без диапазонов потребуются ограничения, они в основном нужны
@Артьер, я до сих пор не понимаю, что ты имеешь в виду. char count = 12; std::string(count, 'c');
вызывает size_type, CharT
ctor, так что же вы имеете в виду, когда говорите, что можно было бы назвать перегрузку двух итераторов? В таком случае? Случай, когда size_type
было просто именем неограниченного параметра шаблона?
@Enlico Если конструктор пары итераторов не был ограничен: godbolt.org/z/xzr1YEe47
Потому что
std::sort
предшествует понятиям