このシグネチャを持つサードパーティ関数があります。
std::vector<T> f(T t);
また、T
という名前のsrc
という既存の無限範囲( 範囲-v3ソート )が存在します。 f
をその範囲のすべての要素にマップし、すべてのベクトルをすべての要素を持つ単一の範囲に平坦化するパイプラインを作成したいと思います。
本能的に、私は次のように書きます。
auto rng = src | view::transform(f) | view::join;
ただし、一時的なコンテナのビューを作成できないため、これは機能しません。
Range-v3はこのような範囲パイプラインをどのようにサポートしますか?
できないのではないかと思います。 view
sには、一時をどこにでも保存するための機構はありません。これは docs からのビューの概念に明示的に反しています。
ビューは、要素の基本的なシーケンスのビューを、変更したりコピーしたりせずに、カスタムの方法で表示する軽量ラッパーです。ビューは作成とコピーが簡単で、非所有参照セマンティクスがあります。
したがって、そのjoin
が機能し、式より長生きするためには、どこかでこれらの一時的な要素を保持する必要があります。その何かはaction
かもしれません。これはうまくいきます( デモ ):
auto rng = src | view::transform(f) | action::join;
明らかにsrc
が無限であることを除いて、そして有限のsrc
であっても、とにかく使用したいオーバーヘッドが多すぎます。
おそらくview::join
をコピー/リライトして、代わりにview::all
(必要な ここ )の微妙に変更されたバージョンを使用する必要がありますその中に)、それが内部的に格納する(そしてその格納されたバージョンにイテレータペアを返す)右辺値コンテナが許可されました。しかし、それは数百行に相当するコードのコピーであるため、たとえそれが機能しても、かなり満足できるものではないようです。
range-v3は、一時的なコンテナのビューを禁止し、ぶら下がりイテレータの作成を回避します。あなたの例は、ビュー構成でこのルールが必要な理由を正確に示しています:
auto rng = src | view::transform(f) | view::join;
view::join
は、begin
によって返された一時ベクトルのend
およびf
イテレータを格納する必要があり、使用される前に無効化されます。
「それはすばらしいことだ、ケーシー、でもrange-v3ビューはこのような一時的な範囲を内部的に保存しないのはなぜですか?」
パフォーマンスだから。イテレータ操作がO(1)であるという要件に基づいてSTLアルゴリズムのパフォーマンスが予測される方法と同様に、ビュー構成のパフォーマンスは、ビュー操作が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
は入力範囲であるため、シングルパスです。
「これは初心者向けではありません。ヘック、エキスパート向けではありません。range-v3で「一時ストレージマテリアライゼーション™」がサポートされていないのはなぜですか?」
私たちはそれに慣れていないので-パッチは歓迎します;)
編集済み
どうやら、以下のコードは、ビューが参照するデータをビューが所有できないというルールに違反しています。 (ただし、このようなものを書くことが厳密に禁止されているかどうかはわかりません。)
ranges::view_facade
を使用してカスタムビューを作成します。 f
が返すベクトルを(一度に1つずつ)保持し、範囲に変更します。これにより、そのような範囲の範囲でview::join
を使用できるようになります。確かに、要素へのランダムアクセスまたは双方向アクセスはできません(ただし、view::join
自体が範囲を入力範囲に低下させる)ことも、要素に割り当てることもできません。
Eric Nieblerの repository からstruct MyRange
をコピーして、少し変更しました。
#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でコンパイルされています。
これは、手の込んだハッキングをあまり必要としない別のソリューションです。 f
を呼び出すたびにstd::make_shared
を呼び出すことになります。しかし、とにかく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';
}
}
もちろん、ここでの問題は、ビューの全体的な概念です。つまり、非保存の階層型レイジーエバリュエーターです。この規約に対応するために、ビューは範囲要素への参照を渡す必要があり、一般に、それらは右辺値と左辺値の両方の参照を処理できます。
残念ながら、この特定のケースでは、_view::transform
_は右辺値参照を提供できます。これは、関数f(T t)
が値でコンテナーを返すためです。_view::join
_は、ビュー(_view::all
_)を内部コンテナーに。
可能な解決策はすべて、パイプラインのどこかにある種の一時的なストレージを導入します。ここに私が思いついたオプションがあります:
view::all
_のバージョンを作成します(Barryの提案に従います)。私の観点から見ると、これは「非保存ビュー」の概念に違反しており、また、手間のかかるテンプレートコーディングが必要であるため、このオプションは推奨されません。_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
_は左辺値参照を返すので、_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を使用することをお勧めします。これにより、セマンティクス全体をそのまま維持でき、実装が非常に簡単になります。