Как написать конвейер диапазона, который использует временные контейнеры?
У меня есть сторонняя функция с этой сигнатурой:
std::vector<T> f(T t);
У меня также есть потенциально бесконечный диапазон (диапазона-v3 сортировки) T
с именем src
. Я хочу создать конвейер, который отображает f
ко всем элементам этого диапазона и выравнивает все векторы в один диапазон со всеми их элементами.
Инстинктивно я бы написал следующее.
auto rng = src | view::transform(f) | view::join;
Однако это не сработает, потому что мы не можем создавать представления временных контейнеров.
Как диапазон-v3 поддерживает такой конвейер диапазона?
Ответы
Ответ 1
Я подозреваю, что просто не могу. Ни один из view
не имеет оборудования для хранения временных файлов в любом месте, что явно противоречит концепции представления из docs:
Вид представляет собой облегченную оболочку, которая представляет собой представление базовой последовательности элементов на каком-то обычном пути без изменения или копирования. Представления являются дешевыми для создания и копирования и имеют не владеющую ссылочную семантику.
Итак, для того, чтобы join
работал и переживал это выражение, что-то где-то должно удерживаться в этих временных рядах. Это может быть action
. Это будет работать (демонстрация):
auto rng = src | view::transform(f) | action::join;
за исключением, очевидно, не для src
бесконечности, и даже для конечного src
, вероятно, добавляет слишком много накладных расходов, чтобы вы все равно хотели использовать.
Вам, вероятно, придется скопировать/переписать view::join
, чтобы вместо этого использовать небольшую измененную версию view::all
(обязательно здесь), которая вместо этого требуя контейнера lvalue (и возвращая в него пару итераторов), разрешен для контейнера rvalue, который он будет хранить внутри (и возвращает пару итераторов в эту сохраненную версию). Но это несколько копий кода на несколько сотен строк, поэтому кажется довольно неудовлетворительным, даже если это работает.
Ответ 2
Edited
По-видимому, приведенный ниже код нарушает правило, согласно которому представления не могут владеть данными, на которые они ссылаются. (Тем не менее, я не знаю, было ли строго запрещено писать что-то вроде этого.)
Я использую ranges::view_facade
для создания настраиваемого представления. Он содержит вектор, возвращаемый f
(по одному за раз), меняя его на диапазон. Это позволяет использовать view::join
в диапазоне таких диапазонов. Разумеется, мы не можем иметь случайный или двунаправленный доступ к элементам (но view::join
сам разлагает диапазон на диапазон ввода), и мы не можем назначить им.
Я скопировал struct MyRange
из Eric Niebler репозиторий, слегка изменив его.
#include <iostream>
#include <range/v3/all.hpp>
using namespace ranges;
std::vector<int> f(int i) {
return std::vector<int>(static_cast<size_t>(i), i);
}
template<typename T>
struct MyRange: ranges::view_facade<MyRange<T>> {
private:
friend struct ranges::range_access;
std::vector<T> data;
struct cursor {
private:
typename std::vector<T>::const_iterator iter;
public:
cursor() = default;
cursor(typename std::vector<T>::const_iterator it) : iter(it) {}
T const & get() const { return *iter; }
bool equal(cursor const &that) const { return iter == that.iter; }
void next() { ++iter; }
// Don't need those for an InputRange:
// void prev() { --iter; }
// std::ptrdiff_t distance_to(cursor const &that) const { return that.iter - iter; }
// void advance(std::ptrdiff_t n) { iter += n; }
};
cursor begin_cursor() const { return {data.begin()}; }
cursor end_cursor() const { return {data.end()}; }
public:
MyRange() = default;
explicit MyRange(const std::vector<T>& v) : data(v) {}
explicit MyRange(std::vector<T>&& v) noexcept : data (std::move(v)) {}
};
template <typename T>
MyRange<T> to_MyRange(std::vector<T> && v) {
return MyRange<T>(std::forward<std::vector<T>>(v));
}
int main() {
auto src = view::ints(1); // infinite list
auto rng = src | view::transform(f) | view::transform(to_MyRange<int>) | view::join;
for_each(rng | view::take(42), [](int i) {
std::cout << i << ' ';
});
}
// Output:
// 1 2 2 3 3 3 4 4 4 4 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9
Скомпилирован с gcc 5.3.0.
Ответ 3
range-v3 запрещает просмотр временных контейнеров, чтобы помочь нам избежать создания икрутера. Ваш пример точно показывает, почему это правило необходимо в отношении композиций:
auto rng = src | view::transform(f) | view::join;
Если view::join
должны были хранить итераторы begin
и end
временного вектора, возвращаемого f
, они были бы признаны недействительными до их использования.
"Что все здорово, Кейси, но почему бы не выбирать диапазоны-v3, хранить временные диапазоны, подобные этому внутри?"
Потому что производительность. Подобно тому, как производительность алгоритмов STL основывается на требовании, чтобы операции итератора были O (1), производительность композиций представлений основывается на требовании, чтобы операции просмотра были O (1). Если бы представления отображали временные диапазоны во внутренних контейнерах "за спиной", тогда сложность операций просмотра и, следовательно, составов стала бы непредсказуемой.
"Хорошо, отлично. Учитывая, что я понимаю весь этот замечательный дизайн, как мне сделать эту работу?!"
Поскольку композиция представления не сохранит временные диапазоны для вас, вам нужно сами их сбросить в какое-то хранилище, например:
#include <iostream>
#include <vector>
#include <range/v3/range_for.hpp>
#include <range/v3/utility/functional.hpp>
#include <range/v3/view/iota.hpp>
#include <range/v3/view/join.hpp>
#include <range/v3/view/transform.hpp>
using T = int;
std::vector<T> f(T t) { return std::vector<T>(2, t); }
int main() {
std::vector<T> buffer;
auto store = [&buffer](std::vector<T> data) -> std::vector<T>& {
return buffer = std::move(data);
};
auto rng = ranges::view::ints
| ranges::view::transform(ranges::compose(store, f))
| ranges::view::join;
unsigned count = 0;
RANGES_FOR(auto&& i, rng) {
if (count) std::cout << ' ';
else std::cout << '\n';
count = (count + 1) % 8;
std::cout << i << ',';
}
}
Заметим, что правильность этого подхода зависит от того, что view::join
является диапазоном ввода и, следовательно, однопроходным.
"Это не дружелюбный к новичкам. Черт, это не полезно для экспертов. Почему нет поддержки" материализации временного хранилища "в диапазоне -3?
Потому что мы не добрались до него - патчи приветствуются;)
Ответ 4
Это еще одно решение, которое не требует особого взлома. Это происходит за счет вызова std::make_shared
при каждом вызове f
. Но вы все равно распределяете и заполняете контейнер в f
, так что, возможно, это приемлемая стоимость.
#include <range/v3/core.hpp>
#include <range/v3/view/iota.hpp>
#include <range/v3/view/transform.hpp>
#include <range/v3/view/join.hpp>
#include <vector>
#include <iostream>
#include <memory>
std::vector<int> f(int i) {
return std::vector<int>(3u, i);
}
template <class Container>
struct shared_view : ranges::view_interface<shared_view<Container>> {
private:
std::shared_ptr<Container const> ptr_;
public:
shared_view() = default;
explicit shared_view(Container &&c)
: ptr_(std::make_shared<Container const>(std::move(c)))
{}
ranges::range_iterator_t<Container const> begin() const {
return ranges::begin(*ptr_);
}
ranges::range_iterator_t<Container const> end() const {
return ranges::end(*ptr_);
}
};
struct make_shared_view_fn {
template <class Container,
CONCEPT_REQUIRES_(ranges::BoundedRange<Container>())>
shared_view<std::decay_t<Container>> operator()(Container &&c) const {
return shared_view<std::decay_t<Container>>{std::forward<Container>(c)};
}
};
constexpr make_shared_view_fn make_shared_view{};
int main() {
using namespace ranges;
auto rng = view::ints | view::transform(compose(make_shared_view, f)) | view::join;
RANGES_FOR( int i, rng ) {
std::cout << i << '\n';
}
}
Ответ 5
Проблема здесь, конечно же, в том, что вся идея представления - не-накопительный многослойный ленивый оценщик. Чтобы не отставать от этого контракта, представления должны передавать ссылки на элементы диапазона, и в целом они могут обрабатывать как ссылки rvalue, так и lvalue.
К сожалению, в этом конкретном случае view::transform
может предоставлять только ссылку rvalue, так как ваша функция f(T t)
возвращает контейнер по значению, а view::join
ожидает lvalue при попытке привязать представления (view::all
) к внутренним контейнерам.
Возможные решения будут включать какое-то временное хранилище где-то в конвейере. Вот варианты, которые я придумал:
- Создайте версию
view::all
, которая может внутренне хранить контейнер, переданный ссылкой rvalue (как было предложено Барри). С моей точки зрения, это нарушает
Концепция "без хранения", а также требует некоторого болезненного шаблона
поэтому я бы предложил против этой опции.
-
Используйте временный контейнер для всего промежуточного состояния после шага view::transform
. Может быть сделано вручную:
auto rng1 = src | view::transform(f)
vector<vector<T>> temp = rng1;
auto rng = temp | view::join;
Или используя action::join
. Это приведет к "преждевременной оценке", не будет работать с бесконечным src
, отбросит некоторую память и в целом будет иметь совершенно другую семантику из вашего первоначального намерения, так что это вряд ли решение, но по крайней мере оно соответствует просматривать контракты класса.
-
Оберните временное хранилище вокруг функции, которую вы передаете в view::transform
. Самый простой пример:
const std::vector<T>& f_store(const T& t)
{
static std::vector<T> temp;
temp = f(t);
return temp;
}
а затем передайте f_store
в view::transform
. Поскольку f_store
возвращает ссылку lvalue, view::join
теперь не будет жаловаться.
Это, конечно, несколько хак и будет работать, только если вы упорядочите весь диапазон в какой-нибудь приемник, например, на выходном контейнере. Я считаю, что он выдержит некоторые простые преобразования, такие как view::replace
или более view::transform
s, но что-то более сложное может попытаться получить доступ к этому хранилищу temp
в непростом порядке.
В этом случае могут использоваться другие типы хранилищ, например. std::map
устранит эту проблему и по-прежнему будет допускать бесконечную src
и ленивую оценку за счет некоторой памяти:
const std::vector<T>& fc(const T& t)
{
static std::map<T, vector<T>> smap;
smap[t] = f(t);
return smap[t];
}
Если ваша функция f
не имеет состояния, этот std::map
также может использоваться для потенциального сохранения некоторых вызовов. Этот подход может быть дополнительно улучшен, если есть способ гарантировать, что элемент больше не потребуется, и удалите его из std::map
, чтобы сохранить память. Однако это зависит от дальнейших шагов трубопровода и оценки.
Поскольку эти 3 решения в значительной степени охватывают все места, чтобы ввести временное хранилище между view::transform
и view::join
, я думаю, что это все варианты, которые у вас есть. Я бы предложил пойти с № 3, так как это позволит вам сохранить целостную семантику неповрежденной, и ее довольно просто реализовать.