Preserving block-allocator for boost::deque? - c++

I want to make boost::container::deque to reuse the freed blocks instead of deallocating them and then allocating new ones.
The only configuration option allowed by boost::container::deque is the compile-time specification of the block size (in terms of either items or bytes count).
Indeed, specifying the block size is something that I want to use, but I also would like to specify the number of blocks that will be preserved after becoming free and reused when new blocks will be required. However, as illustrated here, for boost::container::deque this number is 0, so it deallocates as soon as a block becomes free! I want to make a deque with this number equal to 1.
I see an opportunity to achieve this by specifying a custom allocator. Consider this ugly one:
template < typename Block >
struct PreservingAllocator : std::allocator<Block>
{
using std::allocator<Block>::allocator;
Block* allocate(size_t nn)
{
if (nn == 1) if (auto oldBlock = m_reserve->exchange(nullptr); !!oldBlock) return oldBlock;
return std::allocator<Block>::allocate(nn);
}
void deallocate(Block* block, size_t nn)
{
if (nn == 1) block = m_reserve->exchange(block);
if (!!block) std::allocator<Block>::deallocate(block, nn);
}
private:
static constexpr auto Deleter = [](std::atomic<Block*>* pointer)
{
if (!!pointer) if (auto block = pointer->exchange(nullptr); !!block)
std::allocator<Block>{}.deallocate(block,1);
delete pointer;
};
std::shared_ptr<std::atomic<Block*>> m_reserve = {new std::atomic<Block*>{nullptr},Deleter};
};
So the questions are.
How can I specify an allocator of blocks for boost::container::deque (blocks, not items!)?
If there is a way, then will such specification support allocators with state?
If yes, then will the above-mentioned allocator go for it?
After all, if not in this way, how can I make a deque that will not deallocate at least one of its freed blocks and will reuse it later when a new block will be needed?

This gave me an excuse to play more with allocators. I opted for the polymorphic allocators - although that's only tangentially related¹.
Aside: The relation is that with custom allocators you often want to propagate the allocator to nested types that are allocator-aware. See "Advanced" below
Sample Element Type
struct X {
std::string key, value;
};
It doesn't get much simpler, although it allows us to experiment with sharing
the allocator with the nested strings later.
Tracing Memory Resource
Let's create a tracing memory resource. That's pretty straight forward, and we'll just forward to standard new/delete:
namespace pmr = boost::container::pmr;
struct tracing_resource : pmr::memory_resource {
uint64_t n = 0, total_bytes = 0;
virtual void* do_allocate(std::size_t bytes, std::size_t alignment) override {
n += 1;
total_bytes += bytes;
return pmr::new_delete_resource()->allocate(bytes, alignment);
}
virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
if (p) {
n -= 1;
total_bytes -= bytes;
}
return pmr::new_delete_resource()->deallocate(p, bytes, alignment);
}
virtual bool do_is_equal(const memory_resource& other) const noexcept override {
return pmr::new_delete_resource()->is_equal(other);
}
};
We can inspect n (number of allocation) as well as the total bytes allocated at various points throughout our test code.
A Test Routine
Let's put it together in main, starting with our tracer:
tracing_resource tracer;
Let's mount a pooled resource on that:
pmr::unsynchronized_pool_resource res(&tracer);
auto allocations = [&] {
fmt::print("alloc: #{}, {} bytes, cached = {}\n", tracer.n, tracer.total_bytes, cache_buckets(res));
};
allocations();
This will print
alloc: #0, 0 bytes, cached = {0, 0, 0, 0, 0, 0, 0, 0, 0}
right out of the gate.
Now, let's start (re)allocating some deques in various patterns:
pmr::deque<X> collection(&res);
auto report = [&] {
fmt::print("collection = {}\nalloc: #{}, {} bytes, cached = {}\n", collection, tracer.n, tracer.total_bytes, cache_buckets(res));
};
std::vector data1 { X{"1", "eins"}, {"2", "zwei"}, {"3", "drei"}, };
std::vector data2 { X{"4", "vier"}, {"5", "fuenf"}, {"6", "sechs"}, };
std::vector data3 { X{"7", "sieben"}, {"8", "acht"}, {"9", "neun"}, };
auto i = 0;
for (auto const& data : {data1, data2, data3}) {
for (auto el : data) {
(i%2)
? collection.push_back(el)
: collection.push_front(el);
}
report();
collection.clear();
report();
}
This will append varying sequences at varying ends of the container. We won't
do much mutation, as this will become interesting when we make the strings also
use the pooled resource).
Live Demo
Live On Compiler Explorer
#include <boost/container/pmr/deque.hpp>
#include <boost/container/pmr/unsynchronized_pool_resource.hpp>
// debug output
#include <range/v3/all.hpp>
#include <fmt/ranges.h>
#include <fmt/ostream.h>
#include <iomanip>
namespace pmr = boost::container::pmr;
struct tracing_resource : pmr::memory_resource {
uint64_t n = 0, total_bytes = 0;
virtual void* do_allocate(std::size_t bytes, std::size_t alignment) override {
n += 1;
total_bytes += bytes;
return pmr::new_delete_resource()->allocate(bytes, alignment);
}
virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
if (p) {
n -= 1;
total_bytes -= bytes;
}
return pmr::new_delete_resource()->deallocate(p, bytes, alignment);
}
virtual bool do_is_equal(const memory_resource& other) const noexcept override {
return pmr::new_delete_resource()->is_equal(other);
}
};
struct X {
std::string key, value;
friend std::ostream& operator<<(std::ostream& os, X const& x) {
return os << "(" << std::quoted(x.key) << ", " << std::quoted(x.value) << ")";
}
};
auto cache_buckets(pmr::unsynchronized_pool_resource& res) {
using namespace ::ranges;
return views::iota(0ull)
| views::take_exactly(res.pool_count())
| views::transform([&](auto idx) {
return res.pool_cached_blocks(idx);
});
}
int main() {
tracing_resource tracer;
{
pmr::unsynchronized_pool_resource res(&tracer);
auto allocations = [&] {
fmt::print("alloc: #{}, {} bytes, cached = {}\n", tracer.n, tracer.total_bytes, cache_buckets(res));
};
allocations();
{
pmr::deque<X> collection(&res);
auto report = [&] {
fmt::print("collection = {}\nalloc: #{}, {} bytes, cached = {}\n", collection, tracer.n, tracer.total_bytes, cache_buckets(res));
};
std::vector data1 { X{"1", "eins"}, {"2", "zwei"}, {"3", "drei"}, };
std::vector data2 { X{"4", "vier"}, {"5", "fuenf"}, {"6", "sechs"}, };
std::vector data3 { X{"7", "sieben"}, {"8", "acht"}, {"9", "neun"}, };
auto i = 0;
for (auto const& data : {data1, data2, data3}) {
for (auto el : data) {
(i%2)
? collection.push_back(el)
: collection.push_front(el);
}
report();
collection.clear();
report();
}
}
allocations();
}
fmt::print("alloc: #{}, {} bytes\n", tracer.n, tracer.total_bytes);
}
Prints
alloc: #0, 0 bytes, cached = {0, 0, 0, 0, 0, 0, 0, 0, 0}
collection = {("3", "drei"), ("2", "zwei"), ("1", "eins")}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {("6", "sechs"), ("5", "fuenf"), ("4", "vier")}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {("9", "neun"), ("8", "acht"), ("7", "sieben")}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
alloc: #4, 1864 bytes, cached = {0, 0, 1, 0, 0, 3, 0, 0, 0}
alloc: #0, 0 bytes
Advanced
As promised, we can make the element type allocator aware and show propagation:
#include <boost/container/pmr/string.hpp>
// ...
struct X {
using allocator_type = pmr::polymorphic_allocator<X>;
template<typename K, typename V>
explicit X(K&& key, V&& value, allocator_type a = {})
: key(std::forward<K>(key), a), value(std::forward<V>(value), a) {}
pmr::string key, value;
};
Let's amend the test driver to emplace the elements from string literals:
std::vector data1 { std::pair{"1", "eins"}, {"2", "zwei"}, {"3", "drei"}, };
std::vector data2 { std::pair{"4", "vier"}, {"5", "fuenf"}, {"6", "sechs"}, };
std::vector data3 { std::pair{"7", "sieben"}, {"8", "acht"}, {"9", "neun"}, };
auto i = 0;
for (auto const& data : {data1, data2, data3}) {
for (auto [k,v] : data) {
(i%2)
? collection.emplace_back(k, v)
: collection.emplace_front(k, v);
}
For good measure, let's also mutate one of the nested string values:
collection.at(1).value.append(50, '*'); // thwart SSO
report();
collection.at(1).value = "sept";
report();
Again, demoing Live On Compiler Explorer
#include <boost/container/pmr/deque.hpp>
#include <boost/container/pmr/string.hpp>
#include <boost/container/pmr/unsynchronized_pool_resource.hpp>
// debug output
#include <range/v3/all.hpp>
#include <fmt/ranges.h>
#include <fmt/ostream.h>
#include <iomanip>
namespace pmr = boost::container::pmr;
struct tracing_resource : pmr::memory_resource {
uint64_t n = 0, total_bytes = 0;
virtual void* do_allocate(std::size_t bytes, std::size_t alignment) override {
n += 1;
total_bytes += bytes;
return pmr::new_delete_resource()->allocate(bytes, alignment);
}
virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
if (p) {
n -= 1;
total_bytes -= bytes;
}
return pmr::new_delete_resource()->deallocate(p, bytes, alignment);
}
virtual bool do_is_equal(const memory_resource& other) const noexcept override {
return pmr::new_delete_resource()->is_equal(other);
}
};
struct X {
using allocator_type = pmr::polymorphic_allocator<X>;
template<typename K, typename V>
explicit X(K&& key, V&& value, allocator_type a = {})
: key(std::forward<K>(key), a), value(std::forward<V>(value), a) {}
pmr::string key, value;
friend std::ostream& operator<<(std::ostream& os, X const& x) {
return os << "(" << std::quoted(x.key.c_str()) << ", " << std::quoted(x.value.c_str()) << ")";
}
};
auto cache_buckets(pmr::unsynchronized_pool_resource& res) {
using namespace ::ranges;
return views::iota(0ull)
| views::take_exactly(res.pool_count())
| views::transform([&](auto idx) {
return res.pool_cached_blocks(idx);
});
}
int main() {
tracing_resource tracer;
{
pmr::unsynchronized_pool_resource res(&tracer);
auto allocations = [&] {
fmt::print("alloc: #{}, {} bytes, cached = {}\n", tracer.n, tracer.total_bytes, cache_buckets(res));
};
allocations();
{
pmr::deque<X> collection(&res);
auto report = [&] {
fmt::print("collection = {}\nalloc: #{}, {} bytes, cached = {}\n", collection, tracer.n, tracer.total_bytes, cache_buckets(res));
};
std::vector data1 { std::pair{"1", "eins"}, {"2", "zwei"}, {"3", "drei"}, };
std::vector data2 { std::pair{"4", "vier"}, {"5", "fuenf"}, {"6", "sechs"}, };
std::vector data3 { std::pair{"7", "sieben"}, {"8", "acht"}, {"9", "neun"}, };
auto i = 0;
for (auto const& data : {data1, data2, data3}) {
for (auto [k,v] : data) {
(i%2)
? collection.emplace_back(k, v)
: collection.emplace_front(k, v);
}
report();
collection.at(1).value.append(50, '*'); // thwart SSO
report();
collection.at(1).value = "sept";
report();
collection.clear();
report();
}
}
allocations();
}
fmt::print("alloc: #{}, {} bytes\n", tracer.n, tracer.total_bytes);
}
Prints:
alloc: #0, 0 bytes, cached = {0, 0, 0, 0, 0, 0, 0, 0, 0}
collection = {("3", "drei"), ("2", "zwei"), ("1", "eins")}
alloc: #4, 1864 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {("3", "drei"), ("2", "zwei**************************************************"), ("1", "eins")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {("3", "drei"), ("2", "sept"), ("1", "eins")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 2, 0, 0, 0}
collection = {("6", "sechs"), ("5", "fuenf"), ("4", "vier")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 2, 0, 0, 0}
collection = {("6", "sechs"), ("5", "fuenf**************************************************"), ("4", "vier")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {("6", "sechs"), ("5", "sept"), ("4", "vier")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 2, 0, 0, 0}
collection = {}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 2, 0, 0, 0}
collection = {("9", "neun"), ("8", "acht"), ("7", "sieben")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 1, 0, 0, 0}
collection = {("9", "neun"), ("8", "acht**************************************************"), ("7", "sieben")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {("9", "neun"), ("8", "sept"), ("7", "sieben")}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 0, 0, 1, 0, 0, 0}
collection = {}
alloc: #5, 2008 bytes, cached = {0, 0, 0, 1, 0, 2, 0, 0, 0}
alloc: #5, 2008 bytes, cached = {0, 0, 1, 1, 0, 3, 0, 0, 0}
alloc: #0, 0 bytes
Conclusions
While I chose poymorphic allocators because they support scoped_allocator_adaptor<>-style propagation by default, all of the above can be created with statically typed allocators.
It does demonstrate that if you use a pool allocator, the deque behaviour becomes pooled.
Aside: Pool allocators exist that are able to forgo cleanup, which can be valid in some scenarios, e.g. where the entire memory pool lives on the stack anyways. This is a common allocation optimization technique, allowing large numbers of deallocations/destructions to be skipped.
Addressing some more points of your list:
Q. How can I specify an allocator of blocks for boost::container::deque
(blocks, not items!)?
A. I think the allocator is intrinsically always called for blocks,
because of how deques work.
Q. If there is a way, then will such specification support allocators
with state?
A. Standard library as well as Boost Container should all support
stateful allocators these days. When in doubt, Boost Container has your
back.
Q. If yes, then will the above-mentioned allocator go for it?
A. I didn't look closely at it, but you may put it in the same test-bed
to find out
Q. After all, if not in this way, how can I make a deque that will not
deallocate at least one of its freed blocks and will reuse it later when a
new block will be needed?
A. See above. I'm not sure I understand the exact intent of the "at
least one" clause, but I did note that Boost's deque implementation does do
a "private map" allocation — presumably for some block accounting overhead
— that stays around until the deque object is destroyed. This allocation
doesn't happen at (default) construction, but later.

Couldn't leave well enough alone - I wanted to prove that it can be done with stateful statically-typed allocators just the same.
This builds on
Boost Container's basic_string and deque
Boost Container's scoped_allocator_adaptor to get allocator propagation on uses_allocator-construction
Boost Pool's object_pool for a scoped pool that uses segregated storage, optional block size hinting and ordered allocations with any UserAllocator
Finally, I used my own (surprise! I forgot about that earlier) stateful allocator to complement object_pool¹. It's the allocator in the non_boost namespace.
Note that if you don't mind singleton pools, you can just use Boist's own pool allocators. This is obviously recommended because mine is not maintained by Boost.
Note: I sacrificed the pooling statistics for the sake of cleanliness (it's way messier to add these to the allocator than it is to decorate a polymorphic memory_resource), but I guess the profiler knows best in the end
Live On Compiler Explorer
#include <boost/container/deque.hpp>
#include <boost/container/string.hpp>
#include <boost/container/scoped_allocator.hpp>
#include <boost/pool/pool_alloc.hpp>
// debug output
#include <range/v3/all.hpp>
#include <fmt/ranges.h>
#include <fmt/ostream.h>
#include <iomanip>
namespace bc = boost::container;
namespace non_boost {
template <typename T, typename UserAllocator = boost::default_user_allocator_new_delete>
class fast_pool_allocator
{
public:
typedef T value_type;
typedef UserAllocator user_allocator;
typedef value_type * pointer;
typedef const value_type * const_pointer;
typedef value_type & reference;
typedef const value_type & const_reference;
typedef boost::pool<UserAllocator> pool_type;
typedef typename pool_type::size_type size_type;
typedef typename pool_type::difference_type difference_type;
template <typename U>
struct rebind {
typedef fast_pool_allocator<U, UserAllocator> other;
};
pool_type* _ref;
public:
fast_pool_allocator(pool_type& ref) : _ref(&ref) { }
fast_pool_allocator(fast_pool_allocator const&) = default;
fast_pool_allocator& operator=(fast_pool_allocator const&) = default;
// Not explicit, mimicking std::allocator [20.4.1]
template <typename U>
fast_pool_allocator(const fast_pool_allocator<U, UserAllocator> & other) : _ref(other._ref)
{ }
// Default destructor used.
static pointer address(reference r) { return &r; }
static const_pointer address(const_reference s) { return &s; }
static size_type max_size() { return (std::numeric_limits<size_type>::max)(); }
void construct(const pointer ptr, const value_type & t) { new (ptr) T(t); }
void destroy(const pointer ptr) { ptr->~T(); }
bool operator==(fast_pool_allocator const& rhs) const { return _ref == rhs._ref; }
bool operator!=(fast_pool_allocator const& rhs) const { return _ref != rhs._ref; }
pointer allocate(const size_type n)
{
const pointer ret = (n == 1)
? static_cast<pointer>( (_ref->malloc)() )
: static_cast<pointer>( _ref->ordered_malloc(n) );
if (ret == 0)
boost::throw_exception(std::bad_alloc());
return ret;
}
pointer allocate(const size_type n, const void * const) { return allocate(n); }
pointer allocate()
{
const pointer ret = static_cast<pointer>( (_ref->malloc)() );
if (ret == 0)
boost::throw_exception(std::bad_alloc());
return ret;
}
void deallocate(const pointer ptr, const size_type n)
{
#ifdef BOOST_NO_PROPER_STL_DEALLOCATE
if (ptr == 0 || n == 0)
return;
#endif
if (n == 1)
(_ref->free)(ptr);
else
(_ref->free)(ptr, n);
}
void deallocate(const pointer ptr) { (_ref->free)(ptr); }
};
//Specialization of fast_pool_allocator<void> required to make the allocator standard-conforming.
template<typename UserAllocator>
class fast_pool_allocator<void, UserAllocator> {
public:
typedef void* pointer;
typedef const void* const_pointer;
typedef void value_type;
template <class U> struct rebind {
typedef fast_pool_allocator<U, UserAllocator> other;
};
};
}
template <typename T> using Alloc
= bc::scoped_allocator_adaptor< non_boost::fast_pool_allocator<T> >;
struct X {
using allocator_type = Alloc<X>;
template<typename K, typename V>
explicit X(K&& key, V&& value, allocator_type a)
: key(std::forward<K>(key), a), value(std::forward<V>(value), a) {}
bc::basic_string<char, std::char_traits<char>, Alloc<char> > key, value;
friend std::ostream& operator<<(std::ostream& os, X const& x) {
return os << "(" << std::quoted(x.key.c_str()) << ", " << std::quoted(x.value.c_str()) << ")";
}
};
int main() {
boost::pool<boost::default_user_allocator_new_delete> _pool { sizeof(X) };
Alloc<X> alloc { _pool };
bc::deque<X, Alloc<X> > collection(alloc);
auto dump = [&] { fmt::print("collection = {}\n", collection); };
std::vector data1 { std::pair{"1", "eins"}, {"2", "zwei"}, {"3", "drei"}, };
std::vector data2 { std::pair{"4", "vier"}, {"5", "fuenf"}, {"6", "sechs"}, };
std::vector data3 { std::pair{"7", "sieben"}, {"8", "acht"}, {"9", "neun"}, };
auto i = 0;
for (auto const& data : {data1, data2, data3}) {
for (auto [k,v] : data) {
(i%2)
? collection.emplace_back(k, v)
: collection.emplace_front(k, v);
}
dump();
collection.at(1).value.append(50, '*'); // thwart SSO
dump();
collection.at(1).value = "sept";
dump();
collection.clear();
dump();
}
}
¹ (see Is there some way to use boost::obect_pool with faster free operations and Is there a BOOST pool fixed-sized allocator?)

Related

C++ vector of uint8_t to vector of uint32_t

std::vector<uint8_t> vector1 = { 1, 2, 3, 4 };
I would like to transform the vector above in its uint32_t version. I tried doing:
std::vector<uint32_t> vector2(vector1.begin(), vector2.end());
but this code returns the same array in 32 bit version, so the content is still { 1, 2, 3, 4 }.
What I expect to get is an array of a single value (in this case) of 0x01020304.
Is there any way to achieve that preferably without using for loop?
Thank you.
EDIT:
My solution, don't know if it's a good practice, but from what I learned about vectors it should be completely valid:
std::vector<uint32_t> vector2((uint32_t*)vector1.data(), (uint32_t*)(vector1.data() + vector1.size()*sizeof(uint8_t)));
You can use ranges, either by using the range-v3 library, or the C++20 std::ranges.
std::vector<uint8_t> vector1 = { 1, 2, 3, 4, 5, 6, 7, 8 };
std::vector<uint32_t> vec2 =
vector1
| view::chunk(4)
| views::transform(
[](auto&& range){
return accumulate(
range,
static_cast<uint32_t>(0),
[](auto acc, auto i)
{
return acc << 8 | i;
}
);
})
| to<std::vector>();
vec2 will contain {0x1020304, 0x5060708}.
See it on godbolt: https://godbolt.org/z/6z6ehfKbq
I recommend using a loop to do this for simplicity
#include <cstdint>
#include <exception>
#include <vector>
std::uint32_t
combine(std::uint8_t a,
std::uint8_t b,
std::uint8_t c,
std::uint8_t d)
{
return (std::uint32_t(a) << 24) |
(std::uint32_t(b) << 16) |
(std::uint32_t(c) << 8) |
std::uint32_t(d);
}
std::vector<std::uint32_t>
combine_vector(const std::vector<std::uint8_t>& items)
{
if (items.size() % 4 != 0)
throw std::exception();
std::vector<std::uint32_t> ret;
ret.reserve(items.size() / 4);
for (std::size_t i = 0; i < items.size(); i += 4) {
ret.push_back(
combine(items[i + 0],
items[i + 1],
items[i + 2],
items[i + 3]));
}
return ret;
}
I suspect you would need to implement a custom iterator type to do this without using a raw loop.
I made such an iterator
#include <iterator>
class combine_iterator
{
public:
using value_type = std::uint32_t;
using reference = const value_type&;
using pointer = const value_type*;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag;
combine_iterator(const std::uint8_t* data, std::size_t count) :
m_data(data)
{
if (count % 4 != 0)
throw std::exception();
if (count > 0)
++*this;
}
reference
operator*() const
{
return m_cur_val;
}
pointer
operator->() const
{
return &m_cur_val;
}
friend combine_iterator&
operator++(combine_iterator& rhs)
{
std::uint8_t a = *(rhs.m_data++);
std::uint8_t b = *(rhs.m_data++);
std::uint8_t c = *(rhs.m_data++);
std::uint8_t d = *(rhs.m_data++);
rhs.m_cur_val = combine(a, b, c, d);
return rhs;
}
friend combine_iterator
operator++(combine_iterator& lhs, int)
{
auto cp = lhs;
++lhs;
return cp;
}
friend bool
operator!=(const combine_iterator& lhs, const combine_iterator& rhs)
{
return (lhs.m_data != rhs.m_data);
}
private:
const std::uint8_t* m_data;
std::uint32_t m_cur_val;
};
int main()
{
std::vector<std::uint8_t> data = { 1, 2, 3, 4, 5, 6, 7, 8 };
std::vector<std::uint32_t> res(
combine_iterator(data.data(), data.size()),
combine_iterator(data.data() + data.size(), 0));
}
The iterator contains at least one bug. I'm leaving the bugs in as an educational lesson why using a loop if often easier to get correct than implementing custom iterators. Custom iterators should ideally only be created if we create a container which needs it, or the mental overhead of creating a custom iterator can be justified.

how to read and write non-fixed-length structs to biniary file c++

I have vector of structs:
typedef struct
{
uint64_t id = 0;
std::string name;
std::vector<uint64_t> data;
} entry;
That I want to write to file:
FILE *testFile = nullptr;
testFile = fopen("test.b", "wb");
However the normal method for read/write
fwrite(vector.data(), sizeof vector[0], vector.size(), testFile);
fread(vector.data(), sizeof(entry), numberOfEntries, testFile);
does not work as the size of entry can vary wildly depending on the contents of
std::string name;
std::vector<uint64_t> data;
so I would like methods and pointers about how to do read/writing of this data to/from files.
When dealing with non-fixed size data it's important to keep track of the size somehow. You can simply specify the amount of fixed size elements or byte size of whole structure and calculate needed values when reading the struct. I'm in favour of the first one though it can sometimes make debugging a bit harder.
Here is an example how to make a flexible serialization system.
struct my_data
{
int a;
char c;
std::vector<other_data> data;
}
template<class T>
void serialize(const T& v, std::vector<std::byte>& out)
{
static_assert(false, "Unsupported type");
}
template<class T>
requires std::is_trivially_copy_constructible_v<T>
void serialize(const T& v, std::vector<std::byte>& out)
{
out.resize(std::size(out) + sizeof(T));
std::memcpy(std::data(out) + std::size(out) - sizeof(T), std::bit_cast<std::byte*>(&v), sizeof(T));
}
template<class T>
void serialize<std::vector<T>>(const std::vector<T>& v, std::vector<std::byte>& out)
{
serialize<size_t>(std::size(v), out); // add size
for(const auto& e : v)
serialize<T>(v, out);
}
template<>
void serialize<my_data>(const my_data& v, std::vector<std::byte>& out)
{
serialize(v.a, out);
serialize(v.c, out);
serialize(v.data, out);
}
// And likewise you would do for deserialize
int main()
{
std::vector<std::byte> data;
my_data a;
serialize(a, data);
// write vector of bytes to file
}
This is a tedious job and there are already libraries that do it for you like Google's Flatbuffers, Google's Protobuf or a single header BinaryLove3. Some of them work out of the box with aggregate types (meaning all member variables are public). Here is an example of BinaryLove3 in action.
#include <iostream>
#include <vector>
#include <string>
#include <cstdint>
#include <string>
#include <list>
#include "BinaryLove3.hpp"
struct foo
{
uint32_t v0 = 3;
uint32_t v1 = 2;
float_t v2 = 2.5f;
char v3 = 'c';
struct
{
std::vector<int> vec_of_trivial = { 1, 2, 3 };
std::vector<std::string> vec_of_nontrivial = { "I am a Fox!", "In a big Box!" };
std::string str = "Foxes can fly!";
std::list<int> non_random_access_container = { 3, 4, 5 };
} non_trivial;
struct
{
uint32_t v0 = 1;
uint32_t v1 = 2;
} trivial;
};
auto main() -> int32_t
{
foo out = { 4, 5, 6.7f, 'd', {{5, 4, 3, 2}, {"cc", "dd"}, "Fly me to the moon..." , {7, 8, 9}}, {3, 4} };
auto data = BinaryLove3::serialize(bobux);
foo in;
BinaryLove3::deserialize(data, in);
return int32_t(0);
}

Two pointers alternative to C++ std::vector

In general std::vector object size is 24 bytes, as it is implemented as 3 pointers (each pointer is 8 bytes in size on 64-bit CPUs). These pointers are:
begin of vector
end of vector
end of reserved memory for vector (vector capacity)
Do we have any similar container that offers same interface (except for the capacity feature) but is implemented only with two pointers (or perhaps a pointer + size value)?
It makes sense in some cases like my current project. I hold millions of vectors in memory, but I do not need the distinction between vector size and capacity, and memory usage is turning a bottleneck. I have considered a couple of options:
std::string implementation may be tricky and include things like short string optimization, which may be even worse.
std::unique_ptr to some allocated buffer. The interface is not so convenient as std::vector, and I would end up creating my own class that wraps the pointer plus size of the buffer, which is what I am trying to avoid.
So any ready made alternative? Boost libraries based solutions are accepted.
Here's the absolute minimum to encapsulate a unique_ptr<T[]> in a first-class value-type that models a RandomAccessRange:
#include <memory>
template <typename T>
struct dyn_array {
explicit dyn_array(size_t n)
: _n(n), _data(std::make_unique<T[]>(n)) { }
auto begin() const { return _data.get(); }
auto end() const { return begin() + _n; }
auto begin() { return _data.get(); }
auto end() { return begin() + _n; }
auto size() const { return _n; }
private:
size_t _n {};
std::unique_ptr<T[]> _data;
};
That's 15 lines of code. See it Live
int main() {
using std::begin;
using std::end;
for (int n; (n = size_gen(prng)) != 10;) {
dyn_array<double> data(n);
std::iota(begin(data), end(data), 0);
static_assert(sizeof(data) == 2*sizeof(void*));
fmt::print("Size {} data {}\n", n, data);
}
}
Printing e.g.
Size 8 data {0, 1, 2, 3, 4, 5, 6, 7}
Size 6 data {0, 1, 2, 3, 4, 5}
Size 7 data {0, 1, 2, 3, 4, 5, 6}
Size 6 data {0, 1, 2, 3, 4, 5}
I have commented two versions that
add constructor guides with initializers live
and add copy/move semantics live
BONUS: Single Pointer Size
Building on the above, I realized that the size can be inside the allocation making the size of dyn_array exactly 1 pointer.
Live Demo
#include <memory>
#include <cstdlib> // committing the sin of malloc for optimization
template <typename T>
struct dyn_array {
dyn_array(std::initializer_list<T> init)
: _imp(allocate(init.size(), true))
{ std::uninitialized_move(init.begin(), init.end(), begin()); }
dyn_array(dyn_array const& rhs)
: _imp(allocate(rhs.size(), true))
{ std::uninitialized_copy(rhs.begin(), rhs.end(), begin()); }
dyn_array(dyn_array&& rhs) { rhs.swap(*this); }
dyn_array& operator=(dyn_array rhs) { rhs.swap(*this); }
explicit dyn_array(size_t n = 0)
: _imp(allocate(n))
{ }
auto size() const { return _imp? _imp->_n : 0ull; }
auto begin() const { return _imp? _imp->_data + 0 : nullptr; }
auto begin() { return _imp? _imp->_data + 0 : nullptr; }
auto end() const { return begin() + size(); }
auto end() { return begin() + size(); }
auto empty() const { return size() == 0; }
bool operator==(dyn_array const& rhs) const {
return size() == rhs.size() &&
std::equal(rhs.begin(), rhs.end(), begin());
};
void swap(dyn_array& rhs) {
std::swap(_imp, rhs._imp);
}
private:
struct Impl {
size_t _n;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
T _data[]; // C99 extension
#pragma GCC diagnostic pop
};
struct Deleter {
void operator()(Impl* s) const {
while (s->_n) { s->_data[--(s->_n)].~T(); }
std::free(s);
}
};
using Ptr = std::unique_ptr<Impl, Deleter>;
Ptr _imp;
static Ptr allocate(size_t n, bool uninitialized = false) {
if (!n)
return {};
auto p = std::malloc(sizeof(Impl) + n*sizeof(T)); // could be moreconservative
auto s = Ptr(reinterpret_cast<Impl*>(p));
s->_n = n;
if (!uninitialized)
std::uninitialized_default_construct_n(s->_data, n);
return s;
}
};
Which we can use as:
#include <fmt/ranges.h>
static size_t constructions = 0;
static size_t default_ctor, copy_ctor = 0;
static size_t destructions = 0;
struct Sensor final {
Sensor() { ++constructions; ++default_ctor; }
Sensor(Sensor const&) { ++constructions; ++copy_ctor; }
~Sensor() { ++destructions; }
};
int main() {
fmt::print("With initializers: {}, {}\n",
dyn_array{3.1415f},
dyn_array{"one", "two", "three"});
fmt::print("Without: {}, {}\n",
dyn_array<std::string_view>{3},
dyn_array<float>{1},
dyn_array<int>{}); // empty by default, no allocation
auto a = dyn_array{3,2,1};
fmt::print("sizeof(a) == sizeof(void*)? {}\n", sizeof(a) == sizeof(void*));
auto copy = a;
fmt::print("copy: {} == {}? {}\n", copy, a, (copy == a));
auto move = std::move(copy);
fmt::print("move: {} == {}? {}\n", move, a, (move == a));
fmt::print("copy now moved-from: {}, empty? {}\n", copy, copy.empty());
dyn_array<Sensor>(4); // test destructors
fmt::print("constructions({}) and destructions({}) match? {}\n",
constructions, destructions, constructions == destructions);
fmt::print("all default, no copy ctors? {}\n",
(copy_ctor == 0) && (default_ctor == constructions));
dyn_array { Sensor{}, Sensor{}, Sensor{} };
fmt::print("constructions({}) and destructions({}) match? {}\n",
constructions, destructions, constructions == destructions);
fmt::print("initializers({}) were uninitialized-copied: {}\n",
copy_ctor,
(copy_ctor == 3) && (default_ctor + copy_ctor == constructions));
}
Printing:
With initializers: {3.1415}, {"one", "two", "three"}
Without: {"", "", ""}, {1}
sizeof(a) == sizeof(void*)? true
copy: {3, 2, 1} == {3, 2, 1}? true
move: {3, 2, 1} == {3, 2, 1}? true
copy now moved-from: {}, empty? true
constructions(4) and destructions(4) match? true
all default, no copy ctors? true
constructions(10) and destructions(10) match? true
initializers(3) were uninitialized-copied: true
Of course you can do this without using C99 flexible array members, but I didn't want to meddle with alignment manually right now.
BONUS: [] Indexing, front, back, at accessors
These are really simple to add:
auto& operator[](size_t n) const { return *(begin() + n); }
auto& operator[](size_t n) { return *(begin() + n); }
auto& at(size_t n) const { return n<size()?*(begin() + n): throw std::out_of_range("dyn_array::at"); }
auto& at(size_t n) { return n<size()?*(begin() + n): throw std::out_of_range("dyn_array::at"); }
auto& front() const { return at(0); }
auto& front() { return at(0); }
auto& back() const { return at(size()-1); }
auto& back() { return at(size()-1); }
Of course push_back/erase/etc are out since the size doesn't change after construction.
I do not need to change its size after dynamic allocation (from the comments)
Since you do not need your vectors to be expanded dynamically, std::valarray<T> may be a good alternative. Its size is fixed at construction, so the implementation does not need a third pointer.

merging multiple arrays using boost::join

Is it a better idea to use boost::join to access and change the values of different arrays?
I have defined a member array inside class element.
class element
{
public:
element();
int* get_arr();
private:
int m_arr[4];
}
At different place, I'm accessing these arrays and joined together using boost::join and changing the array values.
//std::vector<element> elem;
auto temp1 = boost::join(elem[0].get_arr(),elem[1].get_arr());
auto joined_arr = boost::join(temp1,elem[2].get_arr());
//now going to change the values of the sub array
for(auto& it:joined_arr)
{
it+= sample[i];
i++;
}
Is this a good idea to modify the values of array in the class as above?
In your code you probably want to join the 4-elements arrays. To do that change the signature of get_arr to:
typedef int array[4];
array& get_arr() { return m_arr; }
So that the array size does not get lost.
Performance-wise there is a non-zero cost for accessing elements through the joined view. A double for loop is going to be most efficient, and easily readable too, e.g.:
for(auto& e : elem)
for(auto& a : e.get_arr())
a += sample[i++];
boost::join returns a more complicated type every composition step. At some point you might exceed the compiler's limits on inlining so that you're going to have a runtime cost¹.
Thinking outside the box, it really looks like you are creating a buffer abstraction that allows you to do scatter/gather like IO with few allocations.
As it happens, Boost Asio has nice abstractions for this², and you could use that: http://www.boost.org/doc/libs/1_66_0/doc/html/boost_asio/reference/MutableBufferSequence.html
As I found out in an earlier iteration of this answer code that abstraction sadly only works for buffers accessed through native char-type elements. That was no good.
So, in this rewrite I present a similar abstraction, which consists of nothing than an "hierarchical iterator" that knows how to iterate a sequence of "buffers" (in this implementation, any range will do).
You can choose to operate on a sequence of ranges directly, e.g.:
std::vector<element> seq(3); // tie 3 elements together as buffer sequence
element& b = seq[1];
Or, without any further change, by reference:
element a, b, c;
std::vector<std::reference_wrapper<element> > seq {a,b,c}; // tie 3 elements together as buffer sequence
The C++ version presented at the bottom demonstrates this approach Live On Coliru
The Iterator Implementation
I've used Boost Range and Boost Iterator:
template <typename Seq,
typename WR = typename Seq::value_type,
typename R = typename detail::unwrap<WR>::type,
typename V = typename boost::range_value<R>::type
>
struct sequence_iterator : boost::iterator_facade<sequence_iterator<Seq,WR,R,V>, V, boost::forward_traversal_tag> {
using OuterIt = typename boost::range_iterator<Seq>::type;
using InnerIt = typename boost::range_iterator<R>::type;
// state
Seq& _seq;
OuterIt _ocur, _oend;
InnerIt _icur, _iend;
static sequence_iterator begin(Seq& seq) { return {seq, boost::begin(seq), boost::end(seq)}; }
static sequence_iterator end(Seq& seq) { return {seq, boost::end(seq), boost::end(seq)}; }
// the 3 facade operations
bool equal(sequence_iterator const& rhs) const {
return ((_ocur==_oend) && (rhs._ocur==rhs._oend))
|| (std::addressof(_seq) == std::addressof(rhs._seq) &&
_ocur == rhs._ocur && _oend == rhs._oend &&
_icur == rhs._icur && _iend == rhs._iend);
}
void increment() {
if (++_icur == _iend) {
++_ocur;
setup();
}
}
V& dereference() const {
assert(_ocur != _oend);
assert(_icur != _iend);
return *_icur;
}
private:
void setup() { // to be called after entering a new sub-range in the sequence
while (_ocur != _oend) {
_icur = boost::begin(detail::get(*_ocur));
_iend = boost::end(detail::get(*_ocur));
if (_icur != _iend)
break;
++_ocur; // skid over, this enables simple increment() logic
}
}
sequence_iterator(Seq& seq, OuterIt cur, OuterIt end)
: _seq(seq), _ocur(cur), _oend(end) { setup(); }
};
That's basically the same kind of iterator as boost::asio::buffers_iterator but it doesn't assume an element type. Now, creating sequence_iterators for any sequence of ranges is as simple as:
template <typename Seq> auto buffers_begin(Seq& seq) { return sequence_iterator<Seq>::begin(seq); }
template <typename Seq> auto buffers_end(Seq& seq) { return sequence_iterator<Seq>::end(seq); }
Implementing Your Test Program
Live On Coliru
// DEMO
struct element {
int peek_first() const { return m_arr[0]; }
auto begin() const { return std::begin(m_arr); }
auto end() const { return std::end(m_arr); }
auto begin() { return std::begin(m_arr); }
auto end() { return std::end(m_arr); }
private:
int m_arr[4] { };
};
namespace boost { // range adapt
template <> struct range_iterator<element> { using type = int*; };
// not used, but for completeness:
template <> struct range_iterator<element const> { using type = int const*; };
template <> struct range_const_iterator<element> : range_iterator<element const> {};
}
#include <algorithm>
#include <iostream>
#include <vector>
template <typename Output, typename Input, typename Operation>
size_t process(Output& output, Input const& input, Operation op) {
auto ib = boost::begin(input), ie = boost::end(input);
auto ob = boost::begin(output), oe = boost::end(output);
size_t n = 0;
for (;ib!=ie && ob!=oe; ++n) {
op(*ob++, *ib++);
}
return n;
}
int main() {
element a, b, c;
std::vector<std::reference_wrapper<element> > seq {a,b,c}; // tie 3 elements together as buffer sequence
//// Also supported, container of range objects directly:
// std::list<element> seq(3); // tie 3 elements together as buffer sequence
// element& b = seq[1];
std::vector<int> const samples {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32
};
using boost::make_iterator_range;
auto input = make_iterator_range(samples);
auto output = make_iterator_range(buffers_begin(seq), buffers_end(seq));
while (auto n = process(output, input, [](int& el, int sample) { el += sample; })) {
std::cout << "Copied " << n << " samples, b starts with " << b.peek_first() << "\n";
input.advance_begin(n);
}
}
Prints
Copied 12 samples, b starts with 5
Copied 12 samples, b starts with 22
Copied 8 samples, b starts with 51
Full Listing, C++11 Compatible
Live On Coliru
#include <boost/iterator/iterator_facade.hpp>
#include <boost/range/iterator_range.hpp>
#include <functional> // std::reference_wrapper
namespace detail {
template<typename T> constexpr T& get(T &t) { return t; }
template<typename T> constexpr T const& get(T const &t) { return t; }
template<typename T> constexpr T& get(std::reference_wrapper<T> rt) { return rt; }
template <typename T> struct unwrap { using type = T; };
template <typename T> struct unwrap<std::reference_wrapper<T> > { using type = T; };
}
template <typename Seq,
typename WR = typename Seq::value_type,
typename R = typename detail::unwrap<WR>::type,
typename V = typename boost::range_value<R>::type
>
struct sequence_iterator : boost::iterator_facade<sequence_iterator<Seq,WR,R,V>, V, boost::forward_traversal_tag> {
using OuterIt = typename boost::range_iterator<Seq>::type;
using InnerIt = typename boost::range_iterator<R>::type;
// state
Seq& _seq;
OuterIt _ocur, _oend;
InnerIt _icur, _iend;
static sequence_iterator begin(Seq& seq) { return {seq, boost::begin(seq), boost::end(seq)}; }
static sequence_iterator end(Seq& seq) { return {seq, boost::end(seq), boost::end(seq)}; }
// the 3 facade operations
bool equal(sequence_iterator const& rhs) const {
return ((_ocur==_oend) && (rhs._ocur==rhs._oend))
|| (std::addressof(_seq) == std::addressof(rhs._seq) &&
_ocur == rhs._ocur && _oend == rhs._oend &&
_icur == rhs._icur && _iend == rhs._iend);
}
void increment() {
if (++_icur == _iend) {
++_ocur;
setup();
}
}
V& dereference() const {
assert(_ocur != _oend);
assert(_icur != _iend);
return *_icur;
}
private:
void setup() { // to be called after entering a new sub-range in the sequence
while (_ocur != _oend) {
_icur = boost::begin(detail::get(*_ocur));
_iend = boost::end(detail::get(*_ocur));
if (_icur != _iend)
break;
++_ocur; // skid over, this enables simple increment() logic
}
}
sequence_iterator(Seq& seq, OuterIt cur, OuterIt end)
: _seq(seq), _ocur(cur), _oend(end) { setup(); }
};
template <typename Seq> auto buffers_begin(Seq& seq) { return sequence_iterator<Seq>::begin(seq); }
template <typename Seq> auto buffers_end(Seq& seq) { return sequence_iterator<Seq>::end(seq); }
// DEMO
struct element {
int peek_first() const { return m_arr[0]; }
auto begin() const { return std::begin(m_arr); }
auto end() const { return std::end(m_arr); }
auto begin() { return std::begin(m_arr); }
auto end() { return std::end(m_arr); }
private:
int m_arr[4] { };
};
namespace boost { // range adapt
template <> struct range_iterator<element> { using type = int*; };
// not used, but for completeness:
template <> struct range_iterator<element const> { using type = int const*; };
template <> struct range_const_iterator<element> : range_iterator<element const> {};
}
#include <algorithm>
#include <iostream>
#include <vector>
template <typename Output, typename Input, typename Operation>
size_t process(Output& output, Input const& input, Operation op) {
auto ib = boost::begin(input), ie = boost::end(input);
auto ob = boost::begin(output), oe = boost::end(output);
size_t n = 0;
for (;ib!=ie && ob!=oe; ++n) {
op(*ob++, *ib++);
}
return n;
}
int main() {
element a, b, c;
std::vector<std::reference_wrapper<element> > seq {a,b,c}; // tie 3 elements together as buffer sequence
//// Also supported, container of range objects directly:
// std::list<element> seq(3); // tie 3 elements together as buffer sequence
// element& b = seq[1];
std::vector<int> const samples {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32
};
using boost::make_iterator_range;
auto input = make_iterator_range(samples);
auto output = make_iterator_range(buffers_begin(seq), buffers_end(seq));
while (auto n = process(output, input, [](int& el, int sample) { el += sample; })) {
std::cout << "Copied " << n << " samples, b starts with " << b.peek_first() << "\n";
input.advance_begin(n);
}
}
¹ I'm ignoring the compile-time cost and the lurking dangers with the pattern of auto x = complicated_range_composition when that complicated range composition contains references to temporaries: this is a frequent source of UB bugs
² which have been adopted by various other libraries, like Boost Beast, Boost Process and seem to have found their way into the Networking TS for C++20: Header <experimental/buffer> synopsis (PDF)

Split range into range of overlapping ranges

I attempting to use the Ranges-V3 library to slice up an container of values into a range of ranges such that neighboring ranges share boundary elements.
Consider the following:
using namespace ranges;
std::vector<int> v = { 1, 2, 3, 0, 4, 0, 5, 0, 6, 7, 8, 0, 0, 9 };
auto myRanges = v | /* something like adjacent split */
for_each( myRanges, []( auto&& range ){ std::cout << range << std::endl;} );
I would like to divide the range into overlapping subranges based whether the region fullfills two criteria:
whether the element has a value of zero
or is adjacent to one or more elements with a value of zero
Desired output:
[1,2,3]
[3,0,4,0,5,0,6]
[6,7,8]
[8,0,0,9]
my attempt:
auto degenerate =
[]( auto&& arg ){
return distance( arg ) < 2;
};
auto myRanges = v | view::split(0) | view::remove_if( degenerate );
for_each( myRanges, []( auto&& range ){ std::cout << range << std::endl;} );
Output:
[1,2,3]
[6,7,8]
I'm at a loss on how I might
"insert" the range from 3 to 6
"append" the range from 8 to 9
If I understand your requirements correctly, you can implement a generator in terms of adjacent_find:
namespace detail {
template<typename IterT, typename SentT>
struct seg_gen_fn {
IterT it_;
SentT end_;
bool parity_ = true;
ranges::iterator_range<IterT> operator ()() {
if (it_ == end_) {
return {it_, it_};
}
auto n = ranges::adjacent_find(
it_, end_,
[p = std::exchange(parity_, !parity_)](auto const a, auto const b) {
return a && !b == p;
}
);
return {
std::exchange(it_, n),
n != end_ ? ranges::next(std::move(n)) : std::move(n)
};
}
};
template<typename RngT>
constexpr auto seg_gen(RngT&& rng)
-> seg_gen_fn<ranges::iterator_t<RngT>, ranges::sentinel_t<RngT>>
{ return {ranges::begin(rng), ranges::end(rng)}; }
} // namespace detail
auto const segmented_view = [](auto&& rng) {
return ranges::view::generate(detail::seg_gen(decltype(rng)(rng)))
| ranges::view::take_while([](auto const& seg) { return !seg.empty(); });
};
int main() {
auto const ns = {1, 2, 3, 0, 4, 0, 5, 0, 6, 7, 8, 0, 0, 9};
ranges::copy(segmented_view(ns), ranges::ostream_iterator<>{std::cout, "\n"});
}
Online Demo
Not exactly as succinct as one might hope... :-[
That's probably fine for one-off code, but a little more work and it can be a lot more reusable:
namespace detail {
namespace tag = ranges::tag;
template<
typename RngT, typename PredT, typename IterT = ranges::iterator_t<RngT>,
typename StateT = ranges::tagged_compressed_tuple<
tag::begin(IterT), tag::end(ranges::sentinel_t<RngT>),
tag::current(bool), tag::fun(ranges::semiregular_t<PredT>)
>
>
struct seg_gen_fn : private StateT {
constexpr seg_gen_fn(RngT&& rng, PredT pred)
: StateT{ranges::begin(rng), ranges::end(rng), true, std::move(pred)}
{ }
ranges::iterator_range<IterT> operator ()() {
StateT& state = *this;
auto& it = state.begin();
if (it == state.end()) {
return {it, it};
}
auto& parity = state.current();
auto n = ranges::adjacent_find(
it, state.end(),
[p = std::exchange(parity, !parity), &pred = state.fun()]
(auto const& a, auto const& b) {
return !pred(a) && pred(b) == p;
}
);
return {
std::exchange(it, n),
n != state.end() ? ranges::next(std::move(n)) : std::move(n)
};
}
};
template<typename RngT, typename PredT>
constexpr seg_gen_fn<RngT, PredT> seg_gen(RngT&& rng, PredT pred) {
return {std::forward<RngT>(rng), std::move(pred)};
}
} // namespace detail
auto const segmented_view = [](auto&& rng, auto pred) {
return ranges::view::generate(detail::seg_gen(decltype(rng)(rng), std::move(pred)))
| ranges::view::take_while([](auto const& seg) { return !seg.empty(); });
};
int main() {
auto const ns = {1, 2, 3, 0, 4, 0, 5, 0, 6, 7, 8, 0, 0, 9};
ranges::copy(
segmented_view(ns, [](auto const n) { return n == 0; }),
ranges::ostream_iterator<>{std::cout, "\n"}
);
}
Online Demo
Concept checking and projections are left as an exercise.