Учитывая std::ranges::range в С++ 20, как я могу определить тип значений в этом диапазоне?
Я хочу написать функцию, которая делает std::vector из произвольного диапазона. Я бы хотел, чтобы эта функция имела красивое явное объявление. Что-то вроде:
template<std::ranges::range Range>
std::vector<std::value_type_t<Range>> make_vector(Range const&);
Следующее, кажется, работает, но объявление не является явным, а реализация уродлива (даже игнорируя тот факт, что он не выделяет правильный размер заранее, где это возможно).
template<std::ranges::range Range>
auto make_vector(Range const& range)
{
using IteratorType = decltype(std::ranges::begin(std::declval<Range&>()));
using DerefType = decltype(*std::declval<IteratorType>());
using T = std::remove_cvref_t<DerefType>;
std::vector<T> retval;
for (auto const& x: range) {
retval.push_back(x);
}
return retval;
}
Есть ли канонический/лучший/более короткий/красивый способ сделать это?
Кстати, есть также iter_value_t, чтобы перейти от типов итераторов к типам значений.
Признак типа, который вы ищете, пишется как std::ranges::range_value_t, а не std::value_type_t.
Кроме того, вся функция, которую вы пытаетесь написать здесь, является просто более ограниченной версией std::ranges::to, появившейся в C++23.
Давайте пройдемся по порядку:
template<std::ranges::range Range>
auto make_vector(Range const& range)
Это проверяет, является ли Range диапазоном, но range не является Range, это const Range. Возможно, что R — это range, а R const — нет, поэтому на самом деле вы не ограничиваете эту функцию должным образом.
Правильным ограничением будет:
template<typename Range>
requires std::ranges::range<Range const>
auto make_vector(Range const& range)
Но тогда это ограничивает вас только константными диапазонами (что является ненужным ограничением), а затем требует, чтобы вы очень осторожно использовали Range const по всему телу (о чем очень легко забыть).
И то, и другое является причиной того, что с диапазонами вы захотите использовать ссылки для пересылки:
template<std::ranges::range Range>
auto make_vector(Range&& range)
Это будет правильно ограничивать вашу функцию.
Следующий:
using IteratorType = decltype(std::ranges::begin(std::declval<Range&>()));
using DerefType = decltype(*std::declval<IteratorType>());
using T = std::remove_cvref_t<DerefType>;
Для этих вещей есть признаки типа:
using IteratorType = std::ranges::iterator_t<Range>;
using DerefType = std::iter_reference_t<IteratorType>;
Или:
using DerefType = std::ranges::range_reference_t<Range>;
Но также, поскольку вам нужен тип значения, это (как уже указывалось):
using T = std::ranges::range_value_t<Range>;
Обратите внимание, что тип значения не обязательно является просто ссылочным типом с удаленными квалификаторами.
Наконец:
std::vector<T> retval;
for (auto const& x: range) {
retval.push_back(x);
}
return retval;
Кроме того, вы захотите, по крайней мере:
if constexpr (std::ranges::sized_range<Range>) {
retval.reserve(std::ranges::size(range));
}
Поскольку, если у нас есть доступный размер, было бы неплохо сократить выделение до одного.
Наконец, в C++23 make_vector(r) можно просто написать std::ranges::to<std::vector>(r). Это обеспечивает оптимизацию распределения, о которой я упоминал, но может быть дополнительно более производительным, поскольку внутренняя конструкция vector позволяет избежать постоянной проверки необходимости дополнительного распределения (что и должен делать push_back).
«Это проверка того, является ли Range range, но range не Range, а const Range». Ну, да. Вы хотите, чтобы компилятор мог остановить вас, если вы делаете с ним не-const вещи. Все так и должно быть.
@NicolBolas OP имеет неправильное ограничение, они проверяют, является ли R range, но используют R const как range - range<R> не подразумевает range<R const>. Существуют типы, для которых верно первое, но не второе (что означало бы, что ограничение проходит, но создание экземпляра все равно не удается). ОП не может делать с ним не-const вещи (если не считать const_cast, я думаю).
@ user17732522 это должен быть ответ, а не комментарий.