Как написать конвейер диапазона, который использует временные контейнеры? - программирование
Подтвердить что ты не робот

Как написать конвейер диапазона, который использует временные контейнеры?

У меня есть сторонняя функция с этой сигнатурой:

std::vector<T> f(T t);

У меня также есть потенциально бесконечный диапазон (диапазона-v3 сортировки) T с именем src. Я хочу создать конвейер, который отображает f ко всем элементам этого диапазона и выравнивает все векторы в один диапазон со всеми их элементами.

Инстинктивно я бы написал следующее.

 auto rng = src | view::transform(f) | view::join;

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

Как диапазон-v3 поддерживает такой конвейер диапазона?

4b9b3361

Ответ 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, так как это позволит вам сохранить целостную семантику неповрежденной, и ее довольно просто реализовать.