diff --git a/CHANGELOG.md b/CHANGELOG.md index 11c020d7..bcff2a83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ Version 198: * flat_buffer improvements +* multi_buffer improvements -------------------------------------------------------------------------------- diff --git a/include/boost/beast/core/impl/multi_buffer.ipp b/include/boost/beast/core/impl/multi_buffer.ipp index b548fd18..c826a156 100644 --- a/include/boost/beast/core/impl/multi_buffer.ipp +++ b/include/boost/beast/core/impl/multi_buffer.ipp @@ -446,17 +446,20 @@ basic_multi_buffer:: template basic_multi_buffer:: -basic_multi_buffer(Allocator const& alloc) noexcept - : boost::empty_value< - base_alloc_type>(boost::empty_init_t(), alloc) +basic_multi_buffer( + Allocator const& alloc) noexcept + : boost::empty_value( + boost::empty_init_t(), alloc) + , max_(alloc_traits::max_size(this->get())) , out_(list_.end()) { } template basic_multi_buffer:: -basic_multi_buffer(std::size_t limit, - Allocator const& alloc) noexcept +basic_multi_buffer( + std::size_t limit, + Allocator const& alloc) noexcept : boost::empty_value< base_alloc_type>(boost::empty_init_t(), alloc) , max_(limit) @@ -466,9 +469,10 @@ basic_multi_buffer(std::size_t limit, template basic_multi_buffer:: -basic_multi_buffer(basic_multi_buffer&& other) noexcept - : boost::empty_value< - base_alloc_type>(boost::empty_init_t(), std::move(other.get())) +basic_multi_buffer( + basic_multi_buffer&& other) noexcept + : boost::empty_value( + boost::empty_init_t(), std::move(other.get())) , max_(other.max_) , in_size_(boost::exchange(other.in_size_, 0)) , in_pos_(boost::exchange(other.in_pos_, 0)) @@ -484,8 +488,9 @@ basic_multi_buffer(basic_multi_buffer&& other) noexcept template basic_multi_buffer:: -basic_multi_buffer(basic_multi_buffer&& other, - Allocator const& alloc) +basic_multi_buffer( + basic_multi_buffer&& other, + Allocator const& alloc) : boost::empty_value< base_alloc_type>(boost::empty_init_t(), alloc) , max_(other.max_) @@ -494,7 +499,7 @@ basic_multi_buffer(basic_multi_buffer&& other, { out_ = list_.end(); copy_from(other); - other.reset(); + other.clear(); } else { @@ -516,11 +521,12 @@ basic_multi_buffer(basic_multi_buffer&& other, template basic_multi_buffer:: -basic_multi_buffer(basic_multi_buffer const& other) - : boost::empty_value< - base_alloc_type>(boost::empty_init_t(), alloc_traits:: - select_on_container_copy_construction( - other.get())) +basic_multi_buffer( + basic_multi_buffer const& other) + : boost::empty_value( + boost::empty_init_t(), alloc_traits:: + select_on_container_copy_construction( + other.get())) , max_(other.max_) , out_(list_.end()) { @@ -529,10 +535,11 @@ basic_multi_buffer(basic_multi_buffer const& other) template basic_multi_buffer:: -basic_multi_buffer(basic_multi_buffer const& other, - Allocator const& alloc) - : boost::empty_value< - base_alloc_type>(boost::empty_init_t(), alloc) +basic_multi_buffer( + basic_multi_buffer const& other, + Allocator const& alloc) + : boost::empty_value( + boost::empty_init_t(), alloc) , max_(other.max_) , out_(list_.end()) { @@ -571,10 +578,9 @@ operator=(basic_multi_buffer&& other) -> { if(this == &other) return *this; - reset(); + clear(); max_ = other.max_; - move_assign(other, std::integral_constant{}); + move_assign(other, pocma{}); return *this; } @@ -586,8 +592,7 @@ basic_multi_buffer& { if(this == &other) return *this; - copy_assign(other, std::integral_constant{}); + copy_assign(other, pocca{}); return *this; } @@ -599,8 +604,7 @@ operator=( basic_multi_buffer const& other) -> basic_multi_buffer& { - reset(); - max_ = other.max_; + clear(); copy_from(other); return *this; } @@ -639,6 +643,56 @@ data() noexcept -> return mutable_data_type(*this); } +template +void +basic_multi_buffer:: +reserve(std::size_t n) +{ + if(n > alloc_traits::max_size(this->get())) + BOOST_THROW_EXCEPTION(std::length_error( + "A basic_multi_buffer exceeded the allocator's maximum size")); + std::size_t total = in_size_; + if(n <= total) + return; + if(out_ != list_.end()) + { + total += out_->size() - out_pos_; + if(n <= total) + return; + auto it = out_; + while(++it != list_.end()) + { + total += it->size(); + if(n <= total) + return; + } + } + BOOST_ASSERT(n > total); + (void)prepare(n - total); +} + +template +void +basic_multi_buffer:: +shrink_to_fit() noexcept +{ + if( out_ != list_.end() && + out_ != list_.iterator_to(list_.back())) + { + list_type extra; + extra.splice(extra.end(), list_, + std::next(out_), list_.end()); + for(auto it = extra.begin(); it != extra.end();) + { + auto& e = *it++; + auto const len = sizeof(e) + e.size(); + e.~element(); + alloc_traits::deallocate(this->get(), + reinterpret_cast(&e), len); + } + } +} + template auto basic_multi_buffer:: @@ -647,7 +701,7 @@ prepare(size_type n) -> { if(in_size_ + n > max_) BOOST_THROW_EXCEPTION(std::length_error{ - "dynamic buffer overflow"}); + "A basic_multi_buffer exceeded its maximum size"}); list_type reuse; std::size_t total = in_size_; // put all empty buffers on reuse list @@ -707,7 +761,7 @@ prepare(size_type n) -> auto& e = *it++; reuse.erase(list_.iterator_to(e)); auto const len = sizeof(e) + e.size(); - alloc_traits::destroy(this->get(), &e); + e.~element(); alloc_traits::deallocate(this->get(), reinterpret_cast(&e), len); } @@ -722,10 +776,8 @@ prepare(size_type n) -> in_size_ * growth_factor - in_size_), 512, n})); - auto& e = *reinterpret_cast(static_cast< - void*>(alloc_traits::allocate(this->get(), - sizeof(element) + size))); - alloc_traits::construct(this->get(), &e, size); + auto &e = *::new(alloc( + sizeof(element) + size)) element(size); list_.push_back(e); if(out_ == list_.end()) out_ = list_.iterator_to(e); @@ -813,7 +865,7 @@ consume(size_type n) noexcept auto& e = list_.front(); list_.erase(list_.iterator_to(e)); auto const len = sizeof(e) + e.size(); - alloc_traits::destroy(this->get(), &e); + e.~element(); alloc_traits::deallocate(this->get(), reinterpret_cast(&e), len); #if BOOST_BEAST_MULTI_BUFFER_DEBUG_CHECK @@ -854,45 +906,17 @@ consume(size_type n) noexcept } template +template void basic_multi_buffer:: -reset() noexcept +copy_from(basic_multi_buffer const& other) { - delete_list(); - list_.clear(); - out_ = list_.end(); - in_size_ = 0; - in_pos_ = 0; - out_pos_ = 0; - out_end_ = 0; -} - -template -void -basic_multi_buffer:: -delete_list() noexcept -{ - for(auto iter = list_.begin(); iter != list_.end();) - { - auto& e = *iter++; - auto const len = sizeof(e) + e.size(); - alloc_traits::destroy(this->get(), &e); - alloc_traits::deallocate(this->get(), - reinterpret_cast(&e), len); - } -} - -template -template -void -basic_multi_buffer:: -copy_from(DynamicBuffer const& buffer) -{ - if(buffer.size() == 0) + clear(); + max_ = other.max_; + if(other.size() == 0) return; - using net::buffer_copy; - commit(buffer_copy( - prepare(buffer.size()), buffer.data())); + commit(net::buffer_copy( + prepare(other.size()), other.data())); } template @@ -903,7 +927,7 @@ move_assign(basic_multi_buffer& other, std::false_type) if(this->get() != other.get()) { copy_from(other); - other.reset(); + other.clear(); } else { @@ -926,6 +950,7 @@ move_assign(basic_multi_buffer& other, std::true_type) noexcept in_pos_ = other.in_pos_; out_pos_ = other.out_pos_; out_end_ = other.out_end_; + max_ = other.max_; other.in_size_ = 0; other.out_ = other.list_.end(); @@ -940,8 +965,6 @@ basic_multi_buffer:: copy_assign( basic_multi_buffer const& other, std::false_type) { - reset(); - max_ = other.max_; copy_from(other); } @@ -951,8 +974,7 @@ basic_multi_buffer:: copy_assign( basic_multi_buffer const& other, std::true_type) { - reset(); - max_ = other.max_; + clear(); this->get() = other.get(); copy_from(other); } @@ -1021,6 +1043,46 @@ swap( lhs.swap(rhs); } +template +void +basic_multi_buffer:: +delete_list() noexcept +{ + for(auto it = list_.begin(); it != list_.end();) + { + auto& e = *it++; + auto const len = sizeof(e) + e.size(); + e.~element(); + alloc_traits::deallocate(this->get(), + reinterpret_cast(&e), len); + } +} + +template +char* +basic_multi_buffer:: +alloc(std::size_t n) +{ + if(n > alloc_traits::max_size(this->get())) + BOOST_THROW_EXCEPTION(std::length_error( + "A basic_multi_buffer exceeded the allocator's maximum size")); + return alloc_traits::allocate(this->get(), n); +} + +template +void +basic_multi_buffer:: +clear() noexcept +{ + delete_list(); + list_.clear(); + out_ = list_.end(); + in_size_ = 0; + in_pos_ = 0; + out_pos_ = 0; + out_end_ = 0; +} + template void basic_multi_buffer:: diff --git a/include/boost/beast/core/multi_buffer.hpp b/include/boost/beast/core/multi_buffer.hpp index ad166a4f..0119b272 100644 --- a/include/boost/beast/core/multi_buffer.hpp +++ b/include/boost/beast/core/multi_buffer.hpp @@ -43,16 +43,16 @@ namespace beast { Objects of this type meet the requirements of @b DynamicBuffer and have the following additional properties: - @li The buffer sequence representing the readable bytes - returned by @ref data is mutable. + @li A mutable buffer sequence representing the readable + bytes is returned by @ref data when `this` is non-const. @li Buffer sequences representing the readable and writable bytes, returned by @ref data and @ref prepare, may have length greater than one. - @li A configurable maximum buffer size may be set upon - construction. Attempts to exceed the buffer size will throw - `std::length_error`. + @li A configurable maximum size may be set upon construction + and adjusted afterwards. Calls to @ref prepare that would + exceed this size will throw `std::length_error`. @li Sequences previously obtained using @ref data remain valid after calls to @ref prepare or @ref commit. @@ -71,6 +71,9 @@ class basic_multi_buffer detail::allocator_traits:: template rebind_alloc; + static bool constexpr default_nothrow = + std::is_nothrow_default_constructible::value; + // Storage for the list of buffers representing the input // and output sequences. The allocation for each element // contains `element` followed by raw storage bytes. @@ -89,6 +92,12 @@ class basic_multi_buffer using const_buffer = net::const_buffer; using mutable_buffer = net::mutable_buffer; + using pocma = typename + alloc_traits::propagate_on_container_move_assignment; + + using pocca = typename + alloc_traits::propagate_on_container_copy_assignment; + static_assert(std::is_base_of::iterator_category>::value, "BidirectionalIterator requirements not met"); @@ -97,8 +106,7 @@ class basic_multi_buffer typename std::iterator_traits::iterator_category>::value, "BidirectionalIterator requirements not met"); - std::size_t max_ = - (std::numeric_limits::max)(); + std::size_t max_; list_type list_; // list of allocated buffers iter out_; // element that contains out_pos_ size_type in_size_ = 0; // size of the input sequence @@ -115,24 +123,26 @@ public: /** Constructor - Upon construction, @ref capacity will return zero. + After construction, @ref capacity will return zero, and + @ref max_size will return the largest value which may + be passed to the allocator's `allocate` function. */ - basic_multi_buffer() noexcept( - std::is_nothrow_default_constructible::value) - : - out_(list_.end()) + basic_multi_buffer() noexcept(default_nothrow) + : max_(alloc_traits::max_size(this->get())) + , out_(list_.end()) { } /** Constructor - Upon construction, @ref capacity will return zero. + After construction, @ref capacity will return zero, and + @ref max_size will return the specified value of `limit`. - @param limit The setting for @ref max_size. + @param limit The desired maximum size. */ explicit - basic_multi_buffer(std::size_t limit) noexcept( - std::is_nothrow_default_constructible::value) + basic_multi_buffer( + std::size_t limit) noexcept(default_nothrow) : max_(limit) , out_(list_.end()) { @@ -140,87 +150,116 @@ public: /** Constructor - Upon construction, @ref capacity will return zero. + After construction, @ref capacity will return zero, and + @ref max_size will return the largest value which may + be passed to the allocator's `allocate` function. - @param alloc The allocator to use. + @param alloc The allocator to use for the object. + + @par Exception Safety + + No-throw guarantee. */ explicit basic_multi_buffer(Allocator const& alloc) noexcept; /** Constructor - Upon construction, @ref capacity will return zero. + After construction, @ref capacity will return zero, and + @ref max_size will return the specified value of `limit`. - @param limit The setting for @ref max_size. + @param limit The desired maximum size. - @param alloc The allocator to use. + @param alloc The allocator to use for the object. + + @par Exception Safety + + No-throw guarantee. */ basic_multi_buffer( std::size_t limit, Allocator const& alloc) noexcept; /** Move Constructor - Constructs the container with the contents of other - using move semantics. After the move, other is - guaranteed to be empty. + The container is constructed with the contents of `other` + using move semantics. The maximum size will be the same + as the moved-from object. - Buffer sequences previously obtained using @ref data - or @ref prepare are not invalidated after the move. + Buffer sequences previously obtained from `other` using + @ref data or @ref prepare remain valid after the move. - @param other The object to move from. After the move, - the moved-from object's state will be as if default - constructed using its current allocator and limit. + @param other The object to move from. After the move, the + moved-from object will have zero capacity, zero readable + bytes, and zero writable bytes. + + @par Exception Safety + + No-throw guarantee. */ basic_multi_buffer(basic_multi_buffer&& other) noexcept; /** Move Constructor - Using alloc as the allocator for the new container, the - contents of other are moved. If `alloc != other.get_allocator()`, - this results in a copy. After the move, other is - guaranteed to be empty. + Using `alloc` as the allocator for the new container, the + contents of `other` are moved. If `alloc != other.get_allocator()`, + this results in a copy. The maximum size will be the same + as the moved-from object. - All buffers sequences previously obtained using - @ref data or @ref prepare are invalidated. + Buffer sequences previously obtained from `other` using + @ref data or @ref prepare become invalid after the move. @param other The object to move from. After the move, - the moved-from object's state will be as if default - constructed using its current allocator and limit. + the moved-from object will have zero capacity, zero readable + bytes, and zero writable bytes. - @param alloc The allocator to use for the newly - constructed object. + @param alloc The allocator to use for the object. + + @throws std::length_error if `other.size()` exceeds the + maximum allocation size of `alloc`. */ - basic_multi_buffer(basic_multi_buffer&& other, + basic_multi_buffer( + basic_multi_buffer&& other, Allocator const& alloc); /** Copy Constructor - The newly constructed object will have a copy of the - allocator and contents of other, and zero writable bytes. + This container is constructed with the contents of `other` + using copy semantics. The maximum size will be the same + as the copied object. @param other The object to copy from. + + @throws std::length_error if `other.size()` exceeds the + maximum allocation size of the allocator. */ basic_multi_buffer(basic_multi_buffer const& other); /** Copy Constructor - The newly constructed object will have a copy of the - specified allocator, a copy of the contents of other, - and zero writable bytes. + This container is constructed with the contents of `other` + using copy semantics and the specified allocator. The maximum + size will be the same as the copied object. @param other The object to copy from. - @param alloc The allocator to use. + @param alloc The allocator to use for the object. + + @throws std::length_error if `other.size()` exceeds the + maximum allocation size of `alloc`. */ basic_multi_buffer(basic_multi_buffer const& other, Allocator const& alloc); /** Copy Constructor - The newly constructed object will have a copy of the - contents of other, and zero writable bytes. + This container is constructed with the contents of `other` + using copy semantics. The maximum size will be the same + as the copied object. @param other The object to copy from. + + @throws std::length_error if `other.size()` exceeds the + maximum allocation size of the allocator. */ template basic_multi_buffer(basic_multi_buffer< @@ -228,52 +267,66 @@ public: /** Copy Constructor - The newly constructed object will have a copy of the - specified allocator, a copy of the contents of other, - and zero writable bytes. + This container is constructed with the contents of `other` + using copy semantics. The maximum size will be the same + as the copied object. @param other The object to copy from. - @param alloc The allocator to use. + @param alloc The allocator to use for the object. + + @throws std::length_error if `other.size()` exceeds the + maximum allocation size of `alloc`. */ template - basic_multi_buffer(basic_multi_buffer< - OtherAlloc> const& other, allocator_type const& alloc); + basic_multi_buffer( + basic_multi_buffer const& other, + allocator_type const& alloc); /** Move Assignment - Assigns the container with the contents of other - using move semantics. After the move, other is - guaranteed to be empty. The previous contents of - this container are deleted. + The container is assigned with the contents of `other` + using move semantics. The maximum size will be the same + as the moved-from object. - Buffer sequences previously obtained using @ref data - or @ref prepare are not invalidated after the move. + Buffer sequences previously obtained from `other` using + @ref data or @ref prepare remain valid after the move. @param other The object to move from. After the move, - the moved-from object's state will be as if default - constructed using its current allocator and limit. + the moved-from object will have zero capacity, zero readable + bytes, and zero writable bytes. */ basic_multi_buffer& operator=(basic_multi_buffer&& other); /** Copy Assignment - The assigned object will have a copy of the allocator - and contents of other, and zero writable bytes. The - previous contents of this container are deleted. + The container is assigned with the contents of `other` + using copy semantics. The maximum size will be the same + as the copied object. + + After the copy, `this` will have zero writable bytes. @param other The object to copy from. + + @throws std::length_error if `other.size()` exceeds the + maximum allocation size of the allocator. */ - basic_multi_buffer& operator=(basic_multi_buffer const& other); + basic_multi_buffer& operator=( + basic_multi_buffer const& other); - /** Copy assignment + /** Copy Assignment - The assigned object will have a copy of the contents - of other, and zero writable bytes. The previous contents - of this container are deleted. + The container is assigned with the contents of `other` + using copy semantics. The maximum size will be the same + as the copied object. + + After the copy, `this` will have zero writable bytes. @param other The object to copy from. + + @throws std::length_error if `other.size()` exceeds the + maximum allocation size of the allocator. */ template basic_multi_buffer& operator=( @@ -286,6 +339,66 @@ public: return this->get(); } + /** Set the maximum allowed capacity + + This function changes the currently configured upper limit + on capacity to the specified value. + + @param n The maximum number of bytes ever allowed for capacity. + + @par Exception Safety + + No-throw guarantee. + */ + void + max_size(std::size_t n) noexcept + { + max_ = n; + } + + /** Guarantee a minimum capacity + + This function adjusts the internal storage (if necessary) + to guarantee space for at least `n` bytes. + + Buffer sequences previously obtained using @ref data remain + valid, while buffer sequences previously obtained using + @ref prepare become invalid. + + @param n The minimum number of byte for the new capacity. + If this value is greater than the maximum size, then the + maximum size will be adjusted upwards to this value. + + @throws std::length_error if n is larger than the maximum + allocation size of the allocator. + + @par Exception Safety + + Strong guarantee. + */ + void + reserve(std::size_t n); + + /** Reallocate the buffer to fit the readable bytes exactly. + + Buffer sequences previously obtained using @ref data or + @ref prepare become invalid. + + @par Exception Safety + + No-throw guarantee. + */ + void + shrink_to_fit() noexcept; + + /// Exchange two dynamic buffers + template + friend + void + swap( + basic_multi_buffer& lhs, + basic_multi_buffer& rhs) noexcept; + //-------------------------------------------------------------------------- #if BOOST_BEAST_DOXYGEN @@ -354,6 +467,10 @@ public: sequence. @throws std::length_error if `size() + n` exceeds `max_size()`. + + @par Exception Safety + + Strong guarantee. */ mutable_buffers_type prepare(size_type n); @@ -372,6 +489,10 @@ public: @param n The number of bytes to append. If this number is greater than the number of writable bytes, all writable bytes are appended. + + @par Exception Safety + + No-throw guarantee. */ void commit(size_type n) noexcept; @@ -386,27 +507,20 @@ public: @param n The number of bytes to remove. If this number is greater than the number of readable bytes, all readable bytes are removed. + + @par Exception Safety + + No-throw guarantee. */ void consume(size_type n) noexcept; - /// Exchange two dynamic buffers - template - friend - void - swap( - basic_multi_buffer& lhs, - basic_multi_buffer& rhs) noexcept; - private: template friend class basic_multi_buffer; - void reset() noexcept; - void delete_list() noexcept; - - template - void copy_from(DynamicBuffer const& other); + template + void copy_from(basic_multi_buffer const&); void move_assign(basic_multi_buffer& other, std::false_type); void move_assign(basic_multi_buffer& other, std::true_type) noexcept; void copy_assign(basic_multi_buffer const& other, std::false_type); @@ -414,6 +528,9 @@ private: void swap(basic_multi_buffer&) noexcept; void swap(basic_multi_buffer&, std::true_type) noexcept; void swap(basic_multi_buffer&, std::false_type) noexcept; + void delete_list() noexcept; + char* alloc(std::size_t n); + void clear() noexcept; void debug_check() const; }; diff --git a/test/beast/core/multi_buffer.cpp b/test/beast/core/multi_buffer.cpp index af56030c..87846eca 100644 --- a/test/beast/core/multi_buffer.cpp +++ b/test/beast/core/multi_buffer.cpp @@ -442,6 +442,14 @@ public: } } + // max_size + { + multi_buffer b{10}; + BEAST_EXPECT(b.max_size() == 10); + b.max_size(32); + BEAST_EXPECT(b.max_size() == 32); + } + // prepare { { @@ -570,6 +578,46 @@ public: BEAST_EXPECT(b.size() == 0); } + // reserve + { + multi_buffer b; + BEAST_EXPECT(b.capacity() == 0); + b.reserve(50); + BEAST_EXPECT(b.capacity() >= 50); + b.prepare(20); + b.commit(20); + b.reserve(50); + BEAST_EXPECT(b.capacity() >= 50); + } + + // shrink to fit + { + { + multi_buffer b; + BEAST_EXPECT(b.capacity() == 0); + b.prepare(50); + BEAST_EXPECT(b.capacity() >= 50); + b.commit(50); + BEAST_EXPECT(b.capacity() >= 50); + b.prepare(75); + BEAST_EXPECT(b.capacity() >= 125); + b.shrink_to_fit(); + BEAST_EXPECT(b.capacity() >= b.size()); + } + { + multi_buffer b; + b.prepare(2000); + BEAST_EXPECT(b.capacity() == 2000); + b.commit(1800); + BEAST_EXPECT(b.size() == 1800); + BEAST_EXPECT(b.capacity() == 2000); + b.prepare(5000); + BEAST_EXPECT(b.capacity() == 6800); + b.shrink_to_fit(); + BEAST_EXPECT(b.capacity() == 2000); + } + } + // swap { {