I have to shepherd P1664 through the C++ ISO WG process for the 4th time, and this time the paper’s been knocked all the way back to the SG9 Ranges Group after already (almost) passing to be in C++20. The design has changed a lot and I’m getting a lot of push back on the new form. It’s safe to say that, by now, I’m pretty goddamn exhausted of the paper and I just want to put the whole thing up in flames, even if it IS critical for my range-based text work to use the reconstructible concept to ensure the API is not a pile of steaming garbage to use.

The story of how I got here isn’t hilarious, but I sure wasn’t expecting a harmless issue I filed in Eric Niebler’s range-v3 to turn into… this:

A man with a tiny domino at the front saying "Can I have a constructor on unbounded_view?" with a gigantic domino at the top saying "5 revisions, 3 blog posts, 1 last-minute plenary rejection, an entire std::ranges Customization Point"

I’m having a tough time convincing the room that the design, as it is, is necessary, or that the way it’s specified is helpful. My examples for the text API are too involved because I have to teach the people involved both the text API and then how reconstructible_range applies to it. That has proven to be a tall order, even though it’s literally deployed in working code and people are already using it to immense success. So, how do I get the people in Study Group 9: Ranges to see it? That’s when Barry Revzin dropped me a hint about his paper in views::split, and I began to develop a way to answer some of the important questions posed by proposing it as an improvement to everyone’s favorite range algorithms: std::views::split.

Eh?

For those of us (like me) living under a rock, C++20 added a new range called std::views::split. It was a response to “why the heck can’t I just split over a string, every single language has a way to split strings, this is ridiculous, the Committee is worthless, waaaaaugh” and the usual barrage of complaints. (Many complaints wherein I took gleeful part, because where in the WORLD is my string split?) It works pretty much as you’d expect in the simple cases, but it broke down in more complex cases. Barry Revzin wrote a short-ish post about std::views::split and it’s problems, so I won’t regale you with all the boring cruft: just know it was bad and silly and Barry fixed it up after that blog post with his paper P2210 - Superior String Splitting. The complex cases were patched for C++23, and std::views::lazy_split and std::views::split were both blessed and placed into the C++ Standard Library. All was well… EXCEPT!

Our Kingdom for Some Convenience!

The problem begins when you’re dealing with ranges and generic code. With a method on std::basic_string<CharT, TraitsT>, the return value of split would always be some std::string_view-like thing. Because it is instead implemented as an algorithm, we lose some premium qualities that non-generic code tied to a std::string or std::string_view would give us. Namely, that we have to return a std::ranges::subrange<some-inscrutable-iterator-type>. Even the “Superior String Splitting” paper acknowledges this shortcoming:

Given a range V, the reference type of split_view<V> should be subrange<iterator_t<V>>. This is a generic solution, that works well for all range categories (as long as they’re forward-or-better).

For splitting a string, this means we get a range of subrange<string::iterator> where we might wish we got a span<char const> or a string_view

P2210, §3.1 “What should the reference type be?”

But, he provides no solution and opts out of trying to make a special reference type, or special-casing char specifically in the backing split_view<…>::iterator code. This is a good API design choice, but that still leaves the user holding the bag here. If someone uses a std::string_view, they should get out a std::string_view from their split algorithm. Similarly, if someone puts a std::span<char> in, they should get a std::span<char> out. And so-on, and so forth. If you’ve been reading my blog for a long time now (or just clicked on the very first link in this blog post), you’ll know that I already have a functional solution to this problem! That’s right, it’s:

🎉 R E C O N S T R U C T I B L E ✨ R A N G E S 🎉

Sweet, sweet, vindication.

‘Course, as vindicating as it is, the 9th time of “hey, all of these things we’re trying to do with drop_view and take_view and other ranges are just us trying to reconstruct the original range (or something better) where possible”, is exhausting. I guess I have to change the image above to say “4 blog posts” instead of “3 blog posts”. Nevertheless, yes, this is a job for P1664 - Reconstructible Ranges. Let’s demonstrate how.

Modifying Split for reconstructible_range

Really, there’s not much that we’re going to change, except the underlying value_type (and, consequently, the reference type) type that is used on split. The long and short of it is that with a few small modifications to the specification and implementation of std::views::split, we can achieve a reconstructed range that, when handed std::span or a std::string_view, returns exactly the type that is put it. In fact, reconstructible_range is so flexible that even if you craft a special zstring (“Null terminated string”, don’t ask me why the colloquial name is “Zee String”) view type, it can still reflexively degrade that zstring type into a std::string_view for viewing only portions of the the given string.

First, let’s start by making a few things reconstructible, yeah?

Step 0: Reconstructible spans, string_views, and zstrings

P1664 takes care of the work of turning span and string_view reconstructible in it’s wording, but it will be illustrative to display how this can be done for your own personal types outside the standard library, like for the zstring class. Here’s the whole null-terminated string view implementation:

struct zstring_sentinel {
	bool operator==(char const* p) const {
		return *p == '\0';
	}
};

struct zstring : std::ranges::view_interface<zstring> {
	char const* p = nullptr;
	constexpr zstring() noexcept = default;
	constexpr zstring(char const* p) noexcept : p(p) { }
	constexpr auto begin() const noexcept { return p; }
	constexpr auto end() const noexcept { return zstring_sentinel{}; }
};

I’ve had to do literally no work here because Barry Revzin already implemented it for us, so we’re just running the ol’ photocopier on that code. This is all that’s needed for a zstring class. The sentinel checks if the char const* p is at its end by checking for the null terminator. The rest of the class is straightforward: a simple begin, a simple end, and a constructor that takes your typical C string pointer. All in all, nothing special going on here. The derivation from view_interface is just to get some easy typedefs and automatically opt into a few behaviors from std::ranges. We could write it out, but if the standard library is actually going to work for us, I am more than happy to let it take care of that disgusting boilerplate.

Now, we need to make the class reconstructible. There’s actually multiple ways to make it reconstructible, which have different purposes:

The order of ADL-based reconstruction points.

We are not going to cover them all, because they all have different use cases. An end-user does not have to implement the hook for each one, and there is a cascading effect where calling // (0) will call // (1), all the way to the end of the list. Users only need to implement the reconstruction point for the level of information that is necessary to unambiguously reconstruct their specific view type.

To learn more, you can click the above image and it will transform into a presentation that will explain the gist of it and the various purposes behind it. But, for the purposes of std::views::split, we are going to work with // (2), since that it most applicable to the types we are discussing here!

Let’s implement the customization point for // (2):

struct zstring_sentinel {
	bool operator==(char const* p) const {
		return *p == '\0';
	}
};

struct zstring : std::ranges::view_interface<zstring> {
	char const* p = nullptr;
	constexpr zstring() = default;
	constexpr zstring(char const* p) : p(p) { }
	constexpr auto begin() const { return p; }
	constexpr auto end() const { return zstring_sentinel{}; }

	// Here!
	friend constexpr std::string_view reconstruct(
		std::in_place_type_t<zstring>,
		const char* f,
		const char* l) noexcept {
		return std::string_view{f, static_cast<std::size_t>(l - f)};
	}
};

is the three-argument form of the proposed std::ranges::reconstruct. It takes:

  • a tag (std::in_place_type_t<Range>), so that users can indicate the intended source of the iterator and sentinel;
  • the first iterator; and,
  • the second iterator (or sentinel).

The reason the tag is necessary is due to potentially unbounded damage to overload sets from matching on any-old-pair of pointers. There are a LOT of things that can be reconstructed from 2 pointers (std::span, std::string_view, Unreal Engine’s TArrayView, kokos::mdspan, and so many more). If we allowed someone to write a single overload that is just reconstruct(T*, T*), all hell would break loose in an instant. Every library would attempt to grab-bag the coveted T*, T* overload for their view type, because theirs is the important one, of course! So, to prevent the grab-bag, and to prevent people from fighting each other over fundamental types, we added the tag. The tag also allows someone to explicitly choose the type they want to target (e.g. zstring), while the person writing the reconstruction point gets to choose what comes out on the other side (e.g., zstring + char* + char* yields std::string_view).

There are also additional protections built into the two-argument version for std::ranges::reconstruct(iterator, sentinel), e.g. that either one or both of iterator or sentinel must be a class type. It was something called to my attention by Christopher DiBella, and neatly solves the problem about accidentally hooking on two pointer types (thanks, dude!).

Notice that we cannot guarantee that the f and the l form a perfect null-terminated string range when passed to our reconstruct extension point. Therefore, we cannot return a zstring. This is especially the case for std::views::split: as we provide split portions of the range, we delimit parts of the original string. This means we have to return a std::string_view instead. With std::ranges::reconstruct, that’s perfectly fine: the extension point happily copes with reconstruction to different types that are not the same as the original. It is imperative we can forge a std::string_view for the parts of a zstring for std::views::split, given only its two iterators and the original type. P1664’s flexibility helps us get there without undue user burden.

We add similar reconstruction points for std::span<T, N>, std::basic_string_view<CharT, Traits>. Here’s a quick ‘n’ dirty implementation:

// P1664, reconstructible range extension points
namespace std {
	template <typename _Ty,
		decltype(::std::dynamic_extent) _Extent,
		typename _It, typename _Sen
	>
		requires ::std::contiguous_iterator<_It>
			&& ::std::contiguous_iterator<_Sen>
	constexpr ::std::span<_Ty> reconstruct(::std::in_place_type_t<::std::span<_Ty, _Extent>>,
		_It __iterator, _Sen __sentinel) noexcept {
		auto __iterator_address = *&__iterator;
		return ::std::span<_Ty>(
			__iterator_address,
			static_cast<::std::size_t>(__sentinel - __iterator)
		);
	}

	template <typename _Ty, typename _Traits, typename _It, typename _Sen>
		requires ::std::contiguous_iterator<_It>
			&& ::std::contiguous_iterator<_Sen>
	constexpr ::std::basic_string_view<_Ty, _Traits> reconstruct(
		::std::in_place_type_t<::std::basic_string_view<_Ty, _Traits>>,
		_It __iterator, _Sen __sentinel) noexcept {
		using _SizeType = typename ::std::basic_string_view<_Ty, _Traits>::size_type;
		auto __iterator_address = &*__iterator;
		_SizeType __ptr_size = static_cast<_SizeType>(
			__sentinel - __iterator
		);
		return ::std::basic_string_view<_Ty, _Traits>(
			__iterator_address,
			__ptr_size
		);
	}
} // namespace std

This implementation is not robust and will crash on MSVC with iterator debugging on. It’s also not safe to perform &*__iterator since that could be the end iterator, which is not dereferenceable. libstdc++, libc++, and the VC++ STL all have wrapper classes, so that dereference is actually hella illegal, even for types which should “logically” be pointers. For a more robust implementation, see the version that is shipping with ztd.text. (Robustness includes using std::to_address or an equivalent wrapper as well as checking for end or 0 so as to avoid the dereference.)

Other std::ranges types are also made reconstructible in the proposal, but we’re not worrying too much about those.

Step 1: Making std::views::split_view Reconstructible

As stated earlier, the change is “simple” to make (or, at least, it’s simple when you’ve got C++ standard library brain poisoning and do this stuff all damn day). The key relies in the specification of the return type for the algorithm used to “find the next part”, which is named — conveniently! — find-next in the C++ standardese:

constexpr subrange<iterator_t<V>> find-next(iterator_t<V> it); // exposition only

Effects: Equivalent to:

auto [b, e] = ranges::search(subrange(it, ranges::end(base_)), pattern_);
if (b != ranges::end(base_) && ranges::empty(pattern_)) {
	++b;
	++e;
}
return {b, e};

Keep in mind, the specification omits std:: and std::ranges, so these types are a bit longer than is currently being presented to us. Right now, the specification explicitly returns std::ranges::subrange<std::ranges::iterator_t<V>>. Note that this means that a fuller expansion is std::ranges::subrange<std::ranges::iterator_t<V>, std::ranges::iterator_t<V>>, which does NOT use the sentinel type of V. This is important for types like zstring, because we actually do not care about presenting a zstring or using zstring_sentinel to mark the end of our split! We care about presenting a half-open range of [const char*, const char*) to the end-user. That means we need to go from the input type (e.g., zstring and its iterator const char*) to a suitable type. Thusly, we make some changes. First, to value_type in the split_view<...>::iterator class:

class split_view<>::iterator {
	// …
	using value_type = reconstruct_t<V, iterator_t<V>, iterator_t<V>>;
	// …
};

And, some changes to the exposition-only find-next function:

constexpr value_type find-next(iterator_t<V> it); // exposition only

Effects: Equivalent to:

auto [b, e] = ranges::search(subrange(it, ranges::end(base_)), pattern_);
if (b != ranges::end(base_) && ranges::empty(pattern_)) {
  ++b;
  ++e;
}
return reconstruct(in_place_type<V>, b, e);

std::ranges::reconstruct is called with the tag and the 2 iterator types. Note that we cannot pass in the range itself and use one of the earlier-shown forms of reconstruction, because we are fundamentally only changing the iterators and we do not want to transfer out all of the guts of the range (the object of type V stored in std::views::split_view, that we are currently chopping up into little pieces to view). This is another reason why we need the tag-based form: it is wasteful in split to forcefully re-construct the range each time, when we know we simply want “a view over the space delimited by these two iterators”.

We also know that std::ranges::reconstruct will always return std::ranges::subrange if it cannot find any reconstruction point. Thus, at its base the reconstruction extension point - when called by users and implementers - will always return something that can be used to at least do work on the desired iterators.

Step 3

That’s it.

Two little changes, and suddenly the following holds true using the semi-cursed nonsense implementation I mocked up in Godbolt:

#include <ranges>

/* blah blah cursed code beware when you
   read the actual Godbolt */

int main () {
	namespace rg = std::ranges;
	// For std::string_view
	{
		using V = rg::revzin_split_view<std::string_view, rg::single_view<char>>;
		static_assert(rg::common_range<V>, "👍!");
		static_assert(rg::contiguous_range<rg::range_reference_t<V>>, "👍!!");
		static_assert(std::is_same_v<rg::range_reference_t<V>, std::string_view>, "👍!!!");
	}

	// For zstring
	{
		using V = rg::revzin_split_view<zstring, rg::single_view<char>>;
		static_assert(rg::common_range<V>, "👍!");
		static_assert(rg::contiguous_range<rg::range_reference_t<V>>, "👍!!");
		static_assert(std::is_same_v<rg::range_reference_t<V>, std::string_view>, "👍!!!");
	}

	return 0;
}

This means that you do not need to spend compiler cycles manually converting between std::ranges::subrange<std::basic_string_view<char, std::char_traits<char>>::iterator, std::basic_string_view<char, std::char_traits<char>>::iterator, std::ranges::sized> and std::string_view. You don’t need to spend time waiting for specific constructors to do range-based construction from contiguous std::ranges::subranges. You don’t need to go beg Unreal Engine provider Epic Games to add special constructors to their TArrayView, or ask kokos::mdspan to add special constructors when they have non-strided views that can be represented as a single contiguous buffer. You can write range-based code, and get out the type you put in.

Are you using quickcpplib::span from Niall Douglas’s LLFIO library, because it is specifically configured to fit the ABI layout of the buffer types of the asynchronous library on a given platform and thus benefits from memcpy-ability into the asynchronous buffers? Great: you can use ranges and get out exactly those types, no questions asked. Are you using ztd.text and working with ztd::text::decode_views and don’t want to have to traverse between std::ranges::subrange<ztd::text::decode_view<LOTS, OF, TEMPLATE, ARGS>::iterator, ztd::text::decode_view<LOTS, OF, TEMPLATE, ARGS>::sentinel> and back to ztd::text::decode_view because you wanted to use a std::ranges algorithm? Excellent: no intermediary nonsense to go through and needless temporary construction spam for you! Your optimizer will be happier, your builds will be happier, and your SSD will sing of the disk space saved from all those nonsense intermediate templates!

With reconstruct, you can put the template bloat behind you and use ranges exactly as they were intended, without fear of using auto and trafficking in some god-awful template explosion that will bloat your object file sizes 5x as it happens to pass through a handful of templated functions. You get to work with the types you already have, with the libraries you expect them to, with the layouts you want, and the characteristics you chose. std::ranges::subrange is a good generic tool, but 9 times out of 10 nobody wants the generic tool. Once I exit generic land and I am delivering features to end users, I want concrete types, with the methods I wrote to get work done, on the types I’ve built up over months or years.

std::ranges::reconstruct gives us that bridge back to being normal human beings, and not into the realm of whatever the hell std::ranges::subrange<__gnu_cxx::__normal_iterator<int*, std::span<int, 18446744073709551615>>> is supposed to be.

Important (and Intentional) Limitations

There is one important note in all of this, however. After all, I specifically showed string_view and span and zstring. What happens if we stick a std::string into the mix? After all, we didn’t write a reconstruction point for std::basic_string<CharT, Traits>. Nor did we write one for std::vector<T, Allocator>. Or, for that matter, boost::static_vector<T, N>. There are a lot of contiguous types where you’d want to boil it down to a std::string_view or std::span, and many times you’d like to also have other types do the same as well (e.g., a __gnu_ext::rope to my::custom_rope_view or similar transformation).

This is intentionally not done here.

The reason for this is because it is too powerful of a transformation. Taking any contiguous iterator and boiling it down to a std::span is dangerous because that loses type information in a way that is impossible to recover in a generic algorithm. Once the range goes in and gets reconstructed, it can be hard (or just straight impossible) to back pedal and retrieve the original range back. This is, fundamentally, why the original changes that sparked P1664 - Hannes Hauswedell’s P1739 - did not make these changes:

… It is, however, possible that future more refined concepts would no longer be modelled by an input type erased in the above fashion. For this reason Casey Carter argued against changing views::all and it was dropped from this proposal.

[The current proposal does not suffer from this uncertainty, because we are preserving the exact input type and there is no erasure.]

Similar to views::all here, std::ranges::reconstruct cannot be written for containers such as std::vector without potentially losing important information (iterator invalidation guarantees, for example). The decision to degrade container types has to be made on an algorithm-by-algorithm basis, and cannot be applied to all containers, ranges, and views generically without loss of generality. There are additional concepts and boolean checks folks can use to aid them in deciding to reconstruct something (or not) in the proposal: we won’t go over them here in too much detail.

Note that it is immaterial that zstring “degrades” into std::string_view, because:

  • it is the only logical choice to make in that scenario;
  • does not lose any information when the zstring_sentinel is completely lost;
  • and, it was a decision made intentionally by the end-user overriding that reconstruct extension point.

Obviously, some users could reach in and do some very disappointing things. But users could also commit great crimes with std::ranges::begin or similar; there is some amount of user responsibility that comes with using this extension point, including “please don’t start trying to reconstruct container types because that can lead to severe performance degradation and compiler errors”!

Wrapping Up

The sample code calls it std::ranges::revzin_split and std::ranges::lazy_revzin_split. Of course, my apologies to Barry Revzin: he’s never seen this article nor does he approve or disapprove of the shenanigans I’m pulling here.

You can see the shenanigans in action in this crime-filled Godbolt link. I’m putting it in the std:: namespace to access some GCC internal shenanigans and have shorter names in the final implementation on Godbolt. (Also, because I’m literally lifting the implementation right out of Barry Revzin’s P2210 and I don’t want to move it all to an external namespace). Note, we’re using the tag_invoke concept rather than just plain ADL. That’s purely out of laziness: when I made the modifications to Barry Revzin’s split implementation, I was working from code I already had. The code I had uses tag_invoke.

As tag_invoke is a separate proposal (P1895), P1664 doesn’t introduce it into the Standard Library for consistency with existing ranges extension points. (Though, I do support tag_invoke in general.) But, it was handy and did allow me to solve some of the issues I was facing with allowing for extension outside of my library easily, in combination with a specific __hijack namespace that allowed people to jump into the extension point space for reconstruct on types they do not own explicitly. (Which, I consider a feature and an important part of P1664’s usability. I have no faith Tim Sweeney gives a single flying poo about my ranges code, this C++20 nonsense, or TArrayView being “reconstructible”; I am definitely going to have to add it to the code myself.) The ADL extension point can manage this just fine on its own.

C++23?

Not a clue. Like I said earlier, I have to roundtrip this idea back through SG9 Ranges. Again. Can you tell I’m looking forward to it, five days from now? And then I also have to go through Library Evolution Working Group. A third time. And then Library Working Group for the wording. Again…

A sheep stares with utter exhaustion at their phone.

… Weeeeeee… … … … …

Hopefully we can get reconstructible ranges in the Standard Library, and give this power not just to std::split, but to every algorithm and every user in C++! 💚

P.S.: The lovely drawing of an exhausted sheep was done by Pixie Cat Supreme!

Title Photo by Allan Mas, from Pexels.

Appendix

Full code crimes dumped here. Compiled on GCC 10.3, using -std=c++20 -Wall -Wextra -O3. Uses Victor Zverovich’s fmtlib for some display. Please don’t open namespace std in your own code 😬.

#include <fmt/format.h>
#include <fmt/ranges.h>

#include <algorithm>
#include <iterator>
#include <ranges>
#include <utility>
#include <type_traits>
#include <span>
#include <string_view>

namespace std {

	namespace __idk_detail {
		template <typename T, template <typename...> class Templ>
		struct __is_specialization_of_impl : ::std::false_type { };
		template <typename... T, template <typename...> class Templ>
		struct __is_specialization_of_impl<Templ<T...>, Templ> : ::std::true_type { };
	} // namespace __idk_detail

	//////
	/// @brief Checks whether the given full, complete type from the first argument is related to the raw template name
	/// provided in the second.
	template <typename T, template <typename...> class Templ>
	using is_specialization_of = __idk_detail::__is_specialization_of_impl<remove_cvref_t<T>, Templ>;

	//////
	/// @brief A @c _v alias for ztd::is_specialization_of.
	template <typename T, template <typename...> class Templ>
	inline constexpr bool is_specialization_of_v = is_specialization_of<T, Templ>::value;

} // namespace std


namespace std {

	//////
	/// @brief The tag_t alias produces the type of the given reconstruction point. It's used to directly hook into the
	/// tag_invoke infrastructure.
	//////
	template <auto& _Name>
	using tag_t = ::std::remove_cv_t<::std::remove_reference_t<decltype(_Name)>>;

	namespace __tginv_detail {
		namespace __adl {

#if 0
			// poison pill
			// This is disabled in general becase the poison pill is actually creating serious problems on MSVC, GCC,
			// and under select conditions Clang. I can imagine that there's some special change made in the standard
			// between C++14/1 and C++20 and on so that this works better but I truthfully cannot be assed to figure
			// it out.
			constexpr void tag_invoke() = delete;
#endif

			template <typename _Tag, typename... _Args>
			constexpr auto __adl_tag_invoke(_Tag&& __tag, _Args&&... __args) noexcept(
			     noexcept(tag_invoke(::std::declval<_Tag>(), ::std::declval<_Args>()...)))
			     -> decltype(tag_invoke(::std::declval<_Tag>(), ::std::declval<_Args>()...)) {
				return tag_invoke(::std::forward<_Tag>(__tag), ::std::forward<_Args>(__args)...);
			}
		} // namespace __adl

		class tag_invoke_fn {
		public:
			template <typename _Tag, typename... _Args>
			constexpr auto operator()(_Tag&& __tag, _Args&&... __args) const
			     noexcept(noexcept(__adl::__adl_tag_invoke(::std::declval<_Tag>(), ::std::declval<_Args>()...)))
			          -> decltype(__adl::__adl_tag_invoke(::std::declval<_Tag>(), ::std::declval<_Args>()...)) {
				return __adl::__adl_tag_invoke(::std::forward<_Tag>(__tag), ::std::forward<_Args>(__args)...);
			}
		};
	} // namespace __tginv_detail

	//////
	/// @brief The tag invoke function.
	///
	//////
	inline constexpr __tginv_detail::tag_invoke_fn tag_invoke {};

	namespace __tginv_detail {
		template <bool, typename _Tag, typename... _Args>
		class __is_nothrow_tag_invocable_i : public ::std::false_type { };

		template <typename _Tag, typename... _Args>
		class __is_nothrow_tag_invocable_i<true, _Tag, _Args...>
		: public ::std::integral_constant<bool, ::std::is_nothrow_invocable_v<decltype(tag_invoke), _Tag, _Args...>> {
		};
	} // namespace __tginv_detail

	//////
	/// @brief Whether or not a given tag type and its arguments are tag invocable.
	///
	//////
	template <typename _Tag, typename... _Args>
	class is_tag_invocable : public ::std::is_invocable<decltype(tag_invoke), _Tag, _Args...> { };

	//////
	/// @brief A @c _v alias for ztd::is_tag_invocable.
	///
	//////
	template <typename _Tag, typename... _Args>
	inline constexpr bool is_tag_invocable_v = is_tag_invocable<_Tag, _Args...>::value;

	//////
	/// @brief Whether or not a given tag type and its arguments are both invocable and marked as a @c noexcept
	/// invocation.
	///
	//////
	template <typename _Tag, typename... _Args>
	class is_nothrow_tag_invocable
	: public __tginv_detail::__is_nothrow_tag_invocable_i<is_tag_invocable_v<_Tag, _Args...>, _Tag, _Args...> { };

	//////
	/// @brief A @c _v alias for ztd::is_nothrow_tag_invocable.
	///
	//////
	template <typename _Tag, typename... _Args>
	inline constexpr bool is_nothrow_tag_invocable_v = is_nothrow_tag_invocable<_Tag, _Args...>::value;

	//////
	/// @brief A class representing the type that results from a tag invocation.
	///
	//////
	template <typename _Tag, typename... _Args>
	using tag_invoke_result = ::std::invoke_result<decltype(tag_invoke), _Tag, _Args...>;

	//////
	/// @brief A @c _t alias that gives the actual type that results from a tag invocation.
	///
	//////
	template <typename _Tag, typename... _Args>
	using tag_invoke_result_t = typename tag_invoke_result<_Tag, _Args...>::type;

    namespace __hijack {
        //////
        /// @brief A token to derive from, which in some cases allows external members to place customization points and
        /// extension functions in the __hijack namespace. Extension points would be defined in the "namespace ztd {
        /// namespace __hijack { /* here */ }}" area.
        //////
        struct token { };
    }

} // namespace std

namespace std { namespace ranges {
	namespace __rng_detail {

		class __reconstruct_fn;

		template <typename _It, typename _Sen>
		static constexpr bool __is_cascade_iterator_reconstruct_noexcept() noexcept {
			if constexpr (((::std::is_class_v<_It> || ::std::is_enum_v<_It>)
				              || (::std::is_class_v<_Sen> || ::std::is_enum_v<_Sen>))
				&& is_tag_invocable_v<__reconstruct_fn, _It, _Sen>) {
				return is_nothrow_tag_invocable_v<__reconstruct_fn, _It, _Sen>;
			}
			else {
				return ::std::is_nothrow_constructible_v<subrange<remove_cvref_t<_It>, remove_cvref_t<_Sen>>, _It,
					_Sen>;
			}
		}

		template <typename _InPlace, typename _It, typename _Sen>
		static constexpr bool __is_cascade_reconstruct_noexcept() noexcept {
			return is_tag_invocable_v<__reconstruct_fn, _InPlace, _It, _Sen>
				? is_nothrow_tag_invocable_v<__reconstruct_fn, _InPlace, _It, _Sen>
				: __is_cascade_iterator_reconstruct_noexcept<_It, _Sen>();
		}

		template <typename _InPlace, typename _Range, typename _It, typename _Sen>
		static constexpr bool __is_cascade_range_iterator_reconstruct_noexcept() noexcept {
			return is_tag_invocable_v<__reconstruct_fn, _InPlace, _Range, _It, _Sen>
				? is_nothrow_tag_invocable_v<__reconstruct_fn, _InPlace, _Range, _It, _Sen>
				: __is_cascade_reconstruct_noexcept<_InPlace, _It, _Sen>();
		}

		template <typename _InPlace, typename _Range>
		static constexpr bool __is_cascade_range_reconstruct_noexcept() noexcept {
			return __is_cascade_range_iterator_reconstruct_noexcept<_InPlace, _Range, ::std::ranges::iterator_t<_Range>,
				::std::ranges::sentinel_t<_Range>>();
		}

		template <typename _InPlaceOrIt, typename _RangeOrSen>
		static constexpr bool __is_cascade_range_reconstruct_or_iterator_reconstruct_noexcept() noexcept {
			if constexpr (::std::is_specialization_of_v<::std::remove_cvref_t<_InPlaceOrIt>, ::std::in_place_type_t>) {
				return __is_cascade_range_reconstruct_noexcept<_InPlaceOrIt, _RangeOrSen>();
			}
			else {
				return __is_cascade_iterator_reconstruct_noexcept<_InPlaceOrIt, _RangeOrSen>();
			}
		}

		class __reconstruct_fn : public ::std::__hijack::token {
		public:
			template <typename _InPlaceTag, typename _It, typename _Sen>
			constexpr auto operator()(
				::std::in_place_type_t<_InPlaceTag> __inplace, _It&& __iterator, _Sen&& __sentinel) const
				noexcept(__is_cascade_reconstruct_noexcept<::std::in_place_type_t<_InPlaceTag>, _It, _Sen>()) {
				if constexpr (is_tag_invocable_v<__reconstruct_fn, ::std::in_place_type_t<_InPlaceTag>, _It,
					              _Sen>) {
					return ::std::tag_invoke(
						*this, __inplace, ::std::forward<_It>(__iterator), ::std::forward<_Sen>(__sentinel));
				}
				else {
					(void)__inplace;
					return (*this)(::std::forward<_It>(__iterator), ::std::forward<_Sen>(__sentinel));
				}
			}

			template <typename _InPlaceTag, typename _Range, typename _It, typename _Sen>
			constexpr decltype(auto) operator()(
				::std::in_place_type_t<_InPlaceTag> __inplace, _Range&& __range, _It&& __it, _Sen&& __sen) const
				noexcept(__is_cascade_range_iterator_reconstruct_noexcept<::std::in_place_type_t<_InPlaceTag>,
				     _Range, _It, _Sen>()) {
				if constexpr (is_tag_invocable_v<__reconstruct_fn, ::std::in_place_type_t<_InPlaceTag>, _Range, _It,
					              _Sen>) {
					return ::std::tag_invoke(*this, __inplace, ::std::forward<_Range>(__range),
						::std::forward<_It>(__it), ::std::forward<_Sen>(__sen));
				}
				else {
					return (*this)(__inplace, ::std::forward<_It>(__it), ::std::forward<_Sen>(__sen));
				}
			}

			template <typename _InPlaceOrIt, typename _RangeOrSen>
			constexpr decltype(auto) operator()(
				_InPlaceOrIt&& __inplace_or_iterator, _RangeOrSen&& __range_or_sentinel) const
				noexcept(
				     __is_cascade_range_reconstruct_or_iterator_reconstruct_noexcept<_InPlaceOrIt, _RangeOrSen>()) {
				if constexpr (is_specialization_of_v<remove_cvref_t<_InPlaceOrIt>, ::std::in_place_type_t>) {
					if constexpr (is_tag_invocable_v<__reconstruct_fn, _InPlaceOrIt, _RangeOrSen>) {
						return ::std::tag_invoke(*this, ::std::forward<_InPlaceOrIt>(__inplace_or_iterator),
							::std::forward<_RangeOrSen>(__range_or_sentinel));
					}
					else {
						return (*this)(::std::forward<_InPlaceOrIt>(__inplace_or_iterator),
							::std::forward<_RangeOrSen>(__range_or_sentinel),
							::std::ranges::begin(__range_or_sentinel),
							::std::ranges::end(__range_or_sentinel));
					}
				}
				else if constexpr (((::std::is_class_v<_InPlaceOrIt> || ::std::is_enum_v<_InPlaceOrIt>)
					                   || (::std::is_class_v<_RangeOrSen> || ::std::is_enum_v<_RangeOrSen>))
					&& is_tag_invocable_v<__reconstruct_fn, _InPlaceOrIt, _RangeOrSen>) {
					return ::std::tag_invoke(*this, ::std::forward<_InPlaceOrIt>(__inplace_or_iterator),
						::std::forward<_RangeOrSen>(__range_or_sentinel));
				}
				else {
					// static_assert(ztd::always_false_v<_InPlaceOrIt>, "fuck off");
					return subrange<remove_cvref_t<_InPlaceOrIt>, remove_cvref_t<_RangeOrSen>>(
						::std::forward<_InPlaceOrIt>(__inplace_or_iterator),
						::std::forward<_RangeOrSen>(__range_or_sentinel));
				}
			}
		};

	} // namespace __rng_detail

	inline namespace _c {
		//////
		/// @brief An implementation of the P1664 (https://wg21.link/p1664 |
		/// https://thephd.dev/_vendor/future_cxx/papers/d1664.html) reconstructible ranges extension point. It is
		/// derives from std::__hijack::token in order for outside implementations to have a common place to put
		/// reconstruction for things outside of their control, without needing to place it in the global namespace or
		/// the immediate ztd::ranges namespace, where there are too many other types that could force asking more
		/// questions about what is in the list for ADL and drive up compile-times.
		inline constexpr __rng_detail::__reconstruct_fn reconstruct = {};
	} // namespace _c

}} // namespace std::ranges

// P1664, reconstructible range extension points
namespace std { namespace __hijack {
    template <typename _Ty, decltype(::std::dynamic_extent) _Extent, typename _It, typename _Sen>
        requires ::std::contiguous_iterator<_It> && ::std::contiguous_iterator<_Sen>
	constexpr ::std::span<_Ty> tag_invoke(std::tag_t<::std::ranges::reconstruct>,
		::std::in_place_type_t<::std::span<_Ty, _Extent>>, _It __iterator, _Sen __sentinel) noexcept {
		if constexpr (!::std::is_integral_v<_Sen>) {
			auto __iterator_address = *&__iterator;
			return ::std::span<_Ty>(__iterator_address, static_cast<::std::size_t>(__sentinel - __iterator));
		}
		else {
			auto __iterator_address = &*__iterator;
			return ::std::span<_Ty>(__iterator_address, __sentinel);
		}
	}

	template <typename _Ty, typename _Traits, typename _It, typename _Sen>
        requires ::std::contiguous_iterator<_It> && ::std::contiguous_iterator<_Sen>
	constexpr ::std::basic_string_view<_Ty, _Traits> tag_invoke(std::tag_t<::std::ranges::reconstruct>,
		::std::in_place_type_t<::std::basic_string_view<_Ty, _Traits>>, _It __iterator, _Sen __sentinel) noexcept {
		using _SizeType = typename ::std::basic_string_view<_Ty, _Traits>::size_type;
		if constexpr (!::std::is_integral_v<_Sen>) {
            auto __iterator_address = &*__iterator;
			_SizeType __ptr_size = static_cast<_SizeType>(__sentinel - __iterator);
			return ::std::basic_string_view<_Ty, _Traits>(__iterator_address, __ptr_size);
		}
		else {
            auto __iterator_address = &*__iterator;
			return ::std::basic_string_view<_Ty, _Traits>(
				__iterator_address, static_cast<_SizeType>(__sentinel));
		}
	}
}} // namespace std::__hijack

namespace std::ranges {

    template <typename _Range, typename _It = ranges::iterator_t<remove_cvref_t<_Range>>,
    typename _Sen = ranges::sentinel_t<remove_cvref_t<_Range>>>
	using reconstruct_t = decltype(ranges::reconstruct(
        ::std::declval<::std::in_place_type_t<remove_cvref_t<_Range>>>(),
		     ::std::declval<_It>(), ::std::declval<_Sen>()
    ));

	template <typename _Tag, typename _Range = _Tag>
	using tag_range_reconstruct_t = decltype(ranges::reconstruct(
		::std::declval<::std::in_place_type_t<remove_cvref_t<_Tag>>>(),
        ::std::declval<_Range>()
    ));

} // namespace std::ranges


// The existing split implementation, as modified by P2210
namespace std::ranges {

template <input_range _Vp, forward_range _Pattern>
requires view<_Vp> && view<_Pattern> &&
    indirectly_comparable<iterator_t<_Vp>, iterator_t<_Pattern>,
                          ranges::equal_to> &&
    (forward_range<_Vp> ||
     __detail::__tiny_range<_Pattern>)class revzin_lazy_split_view
    : public view_interface<revzin_lazy_split_view<_Vp, _Pattern>> {
 private:
  template <bool _Const>
  struct _InnerIter;

  template <bool _Const>
  struct _OuterIter {
   private:
    using _Parent = __detail::__maybe_const_t<_Const, revzin_lazy_split_view>;
    using _Base = __detail::__maybe_const_t<_Const, _Vp>;

    constexpr bool __at_end() const {
      return __current() == ranges::end(_M_parent->_M_base) && !trailing_empty;
    }

    // [range.split.outer] p1
    //  Many of the following specifications refer to the notional member
    //  current of outer-iterator.  current is equivalent to current_ if
    //  V models forward_range, and parent_->current_ otherwise.
    constexpr auto& __current() noexcept {
      if constexpr (forward_range<_Vp>)
        return _M_current;
      else
        return _M_parent->_M_current;
    }

    constexpr auto& __current() const noexcept {
      if constexpr (forward_range<_Vp>)
        return _M_current;
      else
        return _M_parent->_M_current;
    }

    _Parent* _M_parent = nullptr;

    // XXX: _M_current is present only if "V models forward_range"
    [[no_unique_address]] __detail::__maybe_present_t<forward_range<_Vp>,
                                                      iterator_t<_Base>>
        _M_current;

    // P2210: need this extra member on the outer iterator
    bool trailing_empty = false;

   public:
    using iterator_concept =
        conditional_t<forward_range<_Base>, forward_iterator_tag,
                      input_iterator_tag>;
    using iterator_category = input_iterator_tag;
    using difference_type = range_difference_t<_Base>;

    struct value_type : view_interface<value_type> {
     private:
      _OuterIter _M_i = _OuterIter();

     public:
      value_type() = default;

      constexpr explicit value_type(_OuterIter __i) : _M_i(std::move(__i)) {}

      constexpr _InnerIter<_Const> begin() const requires copyable<_OuterIter> {
        return _InnerIter<_Const>{_M_i};
      }

      constexpr _InnerIter<_Const> begin() requires(!copyable<_OuterIter>) {
        return _InnerIter<_Const>{std::move(_M_i)};
      }

      constexpr default_sentinel_t end() const { return default_sentinel; }
    };

    _OuterIter() = default;

    constexpr explicit _OuterIter(_Parent* __parent) requires(
        !forward_range<_Base>)
        : _M_parent(__parent) {}

    constexpr _OuterIter(_Parent* __parent,
                         iterator_t<_Base> __current) requires
        forward_range<_Base> : _M_parent(__parent),
                               _M_current(std::move(__current)) {}

    constexpr _OuterIter(_OuterIter<!_Const> __i) requires _Const
        && convertible_to<iterator_t<_Vp>, iterator_t<_Base>>
        : _M_parent(__i._M_parent), _M_current(std::move(__i._M_current)) {}

    constexpr value_type operator*() const { return value_type{*this}; }

    constexpr _OuterIter& operator++() {
      const auto __end = ranges::end(_M_parent->_M_base);
      if (__current() == __end) {
        // LWG3478: there's no trailing empty range (if there was one before)
        trailing_empty = false;
        return *this;
      }

      const auto [__pbegin, __pend] = subrange{_M_parent->_M_pattern};
      if (__pbegin == __pend) {
        ++__current();
      } else if constexpr (__detail::__tiny_range<_Pattern>) {
        __current() = ranges::find(std::move(__current()), __end, *__pbegin);
        if (__current() != __end) {
          ++__current();
          // LWG3478: if the delimiter is the last element, we still have one more empty to go
          if (__current() == __end) {
            trailing_empty = true;
          }
        }
      } else {
        do {
          auto [__b, __p] = ranges::mismatch(__current(), __end,
                                               __pbegin, __pend);
          if (__p == __pend) {
            __current() = __b;
            // LWG3478: set trailing_empty to true in this case
            if (__current() == __end) {
              trailing_empty = true;
            }
            break;
          }
        } while (++__current() != __end);
      }
      return *this;
    }

    constexpr decltype(auto) operator++(int) {
      if constexpr (forward_range<_Base>) {
        auto __tmp = *this;
        ++*this;
        return __tmp;
      } else
        ++*this;
    }

    friend constexpr bool operator==(const _OuterIter& __x,
                                     const _OuterIter& __y) requires
        forward_range<_Base> {
      // P2210: also check trailing_empty
      return __x._M_current == __y._M_current &&
             __x.trailing_empty == __y.trailing_empty;
    }

    friend constexpr bool operator==(const _OuterIter& __x,
                                     default_sentinel_t) {
      return __x.__at_end();
    };

    friend _OuterIter<!_Const>;
    friend _InnerIter<_Const>;
  };

  template <bool _Const>
  struct _InnerIter {
   private:
    using _Base = __detail::__maybe_const_t<_Const, _Vp>;

    constexpr bool __at_end() const {
      auto [__pcur, __pend] = subrange{_M_i._M_parent->_M_pattern};
      auto __end = ranges::end(_M_i._M_parent->_M_base);
      if constexpr (__detail::__tiny_range<_Pattern>) {
        const auto& __cur = _M_i_current();
        if (__cur == __end) return true;
        if (__pcur == __pend) return _M_incremented;
        return *__cur == *__pcur;
      } else {
        auto __cur = _M_i_current();
        if (__cur == __end) return true;
        if (__pcur == __pend) return _M_incremented;
        do {
          if (*__cur != *__pcur) return false;
          if (++__pcur == __pend) return true;
        } while (++__cur != __end);
        return false;
      }
    }

    static constexpr auto _S_iter_cat() {
      using _Cat =
          typename iterator_traits<iterator_t<_Base>>::iterator_category;
      if constexpr (derived_from<_Cat, forward_iterator_tag>)
        return forward_iterator_tag{};
      else
        return _Cat{};
    }

    constexpr auto& _M_i_current() noexcept { return _M_i.__current(); }

    constexpr auto& _M_i_current() const noexcept { return _M_i.__current(); }

    _OuterIter<_Const> _M_i = _OuterIter<_Const>();
    bool _M_incremented = false;

   public:
    using iterator_concept = typename _OuterIter<_Const>::iterator_concept;
    using iterator_category = decltype(_S_iter_cat());
    using value_type = range_value_t<_Base>;
    using difference_type = range_difference_t<_Base>;

    _InnerIter() = default;

    constexpr explicit _InnerIter(_OuterIter<_Const> __i)
        : _M_i(std::move(__i)) {}

    constexpr decltype(auto) operator*() const { return *_M_i_current(); }

    // P2210: add base() to inner iterator
    constexpr iterator_t<_Base> base()
        const& requires copyable<iterator_t<_Base>> {
      return _M_i_current();
    }
    constexpr iterator_t<_Base> base() && { return std::move(_M_i_current()); }

    constexpr _InnerIter& operator++() {
      _M_incremented = true;
      if constexpr (!forward_range<_Base>)
        if constexpr (_Pattern::size() == 0) return *this;
      ++_M_i_current();
      return *this;
    }

    constexpr decltype(auto) operator++(int) {
      if constexpr (forward_range<_Vp>) {
        auto __tmp = *this;
        ++*this;
        return __tmp;
      } else
        ++*this;
    }

    friend constexpr bool operator==(const _InnerIter& __x,
                                     const _InnerIter& __y) requires
        forward_range<_Base> {
      return __x._M_i == __y._M_i;
    }

    friend constexpr bool operator==(const _InnerIter& __x,
                                     default_sentinel_t) {
      return __x.__at_end();
    }

    friend constexpr decltype(auto) iter_move(const _InnerIter& __i) noexcept(
        noexcept(ranges::iter_move(__i._M_i_current()))) {
      return ranges::iter_move(__i._M_i_current());
    }

    friend constexpr void
    iter_swap(const _InnerIter& __x, const _InnerIter& __y) noexcept(noexcept(
        ranges::iter_swap(__x._M_i_current(), __y._M_i_current()))) requires
        indirectly_swappable<iterator_t<_Base>> {
      ranges::iter_swap(__x._M_i_current(), __y._M_i_current());
    }
  };

  _Pattern _M_pattern = _Pattern();
  // XXX: _M_current is "present only if !forward_range<V>"
  [[no_unique_address]] __detail::__maybe_present_t<!forward_range<_Vp>,
                                                    iterator_t<_Vp>>
      _M_current;
  _Vp _M_base = _Vp();

 public:
  revzin_lazy_split_view() = default;

  constexpr revzin_lazy_split_view(_Vp __base, _Pattern __pattern)
      : _M_pattern(std::move(__pattern)), _M_base(std::move(__base)) {}

  template <input_range _Range>
  requires constructible_from<_Vp, views::all_t<_Range>> &&
      constructible_from<_Pattern, single_view<range_value_t<_Range>>>
  constexpr revzin_lazy_split_view(_Range&& __r, range_value_t<_Range> __e)
      : _M_pattern(std::move(__e)),
        _M_base(views::all(std::forward<_Range>(__r))) {}

  constexpr _Vp base() const& requires copy_constructible<_Vp> {
    return _M_base;
  }

  constexpr _Vp base() && { return std::move(_M_base); }

  constexpr auto begin() {
    if constexpr (forward_range<_Vp>)
      return _OuterIter<__detail::__simple_view<_Vp>>{this,
                                                      ranges::begin(_M_base)};
    else {
      _M_current = ranges::begin(_M_base);
      return _OuterIter<false>{this};
    }
  }

  constexpr auto begin() const requires forward_range<_Vp> &&
      forward_range<const _Vp> {
    return _OuterIter<true>{this, ranges::begin(_M_base)};
  }

  constexpr auto end() requires forward_range<_Vp> && common_range<_Vp> {
    return _OuterIter<__detail::__simple_view<_Vp>>{this, ranges::end(_M_base)};
  }

  constexpr auto end() const {
    if constexpr (forward_range<_Vp> && forward_range<const _Vp> &&
                  common_range<const _Vp>)
      return _OuterIter<true>{this, ranges::end(_M_base)};
    else
      return default_sentinel;
  }
};

template <typename _Range, typename _Pred>
revzin_lazy_split_view(_Range&&, _Pred&&)
    -> revzin_lazy_split_view<views::all_t<_Range>, views::all_t<_Pred>>;

template <input_range _Range>
revzin_lazy_split_view(_Range&&, range_value_t<_Range>)
    -> revzin_lazy_split_view<views::all_t<_Range>,
                       single_view<range_value_t<_Range>>>;

namespace views {
inline constexpr __adaptor::_RangeAdaptor revzin_lazy_split =
    []<viewable_range _Range, typename _Fp>(_Range&& __r, _Fp&& __f) {
      return revzin_lazy_split_view{std::forward<_Range>(__r), std::forward<_Fp>(__f)};
    };
}  // namespace views
}  // namespace std::ranges

// The new split implementation from P2210, here named revzin_split/revzin_split_view
namespace std::ranges {

template <forward_range V, forward_range Pattern>
requires view<V> && view<Pattern> &&
    indirectly_comparable<iterator_t<V>, iterator_t<Pattern>, ranges::equal_to>
class revzin_split_view : public view_interface<revzin_split_view<V, Pattern>> {
 private:
  using _ReV = ::std::ranges::reconstruct_t<V, iterator_t<V>, iterator_t<V>>;
  V base_ = V();
  Pattern pattern_ = Pattern();
  struct iterator;
  struct sentinel;

  struct iterator {
  private:
    friend sentinel;
    revzin_split_view* parent_ = nullptr;
    iterator_t<_ReV> cur_ = iterator_t<_ReV>();
    _ReV next_ = _ReV();
    bool trailing_empty_ = false;

   public:
    using iterator_concept = forward_iterator_tag;
    using iterator_category = input_iterator_tag;
    using value_type = _ReV;
    using difference_type = range_difference_t<V>;

    iterator() = default;
    constexpr iterator(revzin_split_view& parent, iterator_t<V> current,
                       _ReV next)
        : parent_(&parent), cur_(current), next_(next) {}

    constexpr iterator_t<V> base() const { return cur_; }
    constexpr value_type operator*() const { return ::std::ranges::reconstruct(::std::in_place_type<V>, cur_, next_.begin()); }

    constexpr iterator& operator++() {
      cur_ = next_.begin();
      if (cur_ != ranges::end(parent_->base_)) {
        cur_ = next_.end();
        if (cur_ == ranges::end(parent_->base_)) {
          trailing_empty_ = true;
          next_ = {cur_, cur_};
        } else {
          next_ = parent_->find_next(cur_);
        }
      } else {
        trailing_empty_ = false;
      }
      return *this;
    }
    constexpr iterator operator++(int) {
      auto tmp = *this;
      ++*this;
      return tmp;
    }

    constexpr bool operator==(const iterator& rhs) const {
      return cur_ == rhs.cur_ && trailing_empty_ == rhs.trailing_empty_;
    }
  };

  struct sentinel {
  private:
    sentinel_t<V> end_ = sentinel_t<V>();
    
  public:
    sentinel() = default;
    constexpr explicit sentinel(revzin_split_view& parent)
        : end_(ranges::end(parent.base_))
    { }
    
    constexpr bool operator==(const iterator& x) const {
        return x.cur_ == end_ && !x.trailing_empty_;
    }
  };

 public:
  revzin_split_view() = default;
  constexpr revzin_split_view(V base, Pattern pattern)
      : base_(std::move(base)), pattern_(std::move(pattern)) {}

  template <forward_range R>
  requires constructible_from<V, views::all_t<R>> &&
      constructible_from<Pattern, single_view<range_value_t<R>>>
  constexpr revzin_split_view(R&& r, range_value_t<R> e)
      : base_(views::all(std::forward<R>(r))),
        pattern_(views::single(std::move(e))) {}

  constexpr V base() const& requires copyable<V> { return base_; }
  constexpr V base() && { return std::move(base_); }

  constexpr iterator begin() {
    return {*this, ranges::begin(base_), find_next(ranges::begin(base_))};
  }

  constexpr auto end() {
    if constexpr (common_range<V>) {
      return iterator{*this, ranges::end(base_), {}};
    } else {
      return sentinel{*this};
    }
  }

  constexpr _ReV find_next(iterator_t<_ReV> it) {
    auto [b, e] = ranges::search(subrange(it, ranges::end(base_)), pattern_);
    if (b != ranges::end(base_) && ranges::empty(pattern_)) {
      ++b;
      ++e;
    }
    return ::std::ranges::reconstruct(::std::in_place_type<V>, b, e);
  }
};

template <class R, class P>
revzin_split_view(R&&, P&&) -> revzin_split_view<views::all_t<R>, views::all_t<P>>;

template <forward_range R>
revzin_split_view(R&&, range_value_t<R>)
    -> revzin_split_view<views::all_t<R>, single_view<range_value_t<R>>>;

namespace views {
inline constexpr __adaptor::_RangeAdaptor revzin_split =
    []<viewable_range _Range, typename _Fp>(_Range&& __r, _Fp&& __f) {
      return revzin_split_view{std::forward<_Range>(__r), std::forward<_Fp>(__f)};
    };
}
}

struct zstring_sentinel {
    bool operator==(char const* p) const {
        return *p == '\0';
    }
};

struct zstring : std::ranges::view_interface<zstring> {
    char const* p = nullptr;
    constexpr zstring() = default;
    constexpr zstring(char const* p) : p(p) { }
    constexpr auto begin() const { return p; }
    constexpr auto end() const { return zstring_sentinel{}; }

    friend inline constexpr zstring tag_invoke(std::tag_t<std::ranges::reconstruct>,
        const char* f,
        zstring_sentinel) {
        return {f};
    }

    friend inline constexpr std::string_view tag_invoke(std::tag_t<std::ranges::reconstruct>,
        std::in_place_type_t<zstring>,
        const char* f,
        const char* l) {
        return std::string_view{f, static_cast<std::size_t>(l - f)};
    }
};

template <typename R>
concept forward_only_range =
    std::ranges::forward_range<R>
    && !std::ranges::bidirectional_range<R>;

int main() {
  namespace rg = std::ranges;
  static_assert(rg::view<zstring>);
  {
    // the new one is a forward range of contiguous ranges
    using V = rg::revzin_split_view<zstring, rg::single_view<char>>;
    static_assert(rg::view<V>);
    static_assert(forward_only_range<V>);
    static_assert(!rg::common_range<V>);
    static_assert(rg::contiguous_range<rg::range_reference_t<V>>);
    static_assert(std::is_same_v<rg::range_reference_t<V>, std::string_view>);

    // the old one is a forward range of forward ranges
    using LV = rg::revzin_lazy_split_view<zstring, rg::single_view<char>>;
    static_assert(rg::view<LV>);
    static_assert(forward_only_range<LV>);
    static_assert(!rg::common_range<LV>);
    static_assert(forward_only_range<rg::range_reference_t<LV>>);
  }

  {
    // preserve common
    using V = rg::revzin_split_view<std::string_view, std::string_view>;
    static_assert(forward_only_range<V>);
    static_assert(rg::common_range<V>);
    static_assert(rg::contiguous_range<rg::range_reference_t<V>>);
    static_assert(std::is_same_v<rg::range_reference_t<V>, std::string_view>);
  }

  std::string_view test = "meow";
  auto re_test = std::ranges::reconstruct(std::in_place_type<std::string_view>, test);
  static_assert(std::is_same_v<decltype(test), decltype(re_test)>);

  using namespace std::literals;
  fmt::print("revzin_split\n");
  fmt::print("{}\n", "a b c"sv | std::views::revzin_split(' '));
  fmt::print("{}\n", "a b c "sv | std::views::revzin_split(' '));
  fmt::print("{}\n", "abc"sv | std::views::revzin_split(""sv));
  fmt::print("{}\n", std::views::revzin_split("xxyx"sv, "xy"sv));
  fmt::print("{}\n", zstring{" x  "} | std::views::revzin_split(' '));

  fmt::print("revzin_lazy_split\n");
  fmt::print("{}\n", "a b c"sv | std::views::revzin_lazy_split(' '));
  fmt::print("{}\n", "a b c "sv | std::views::revzin_lazy_split(' '));
  fmt::print("{}\n", "abc"sv | std::views::revzin_lazy_split(""sv));
  fmt::print("{}\n", std::views::revzin_lazy_split("xxyx"sv, "xy"sv));
  fmt::print("{}\n", zstring{" x  "} | std::views::revzin_lazy_split(' '));
}