2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-19 04:42:09 +00:00

Makes flat_tree a proper container (#383)

Removes flat_tree::get_view()
Adds flat_tree{iterator, reverse_iterator, begin, end, rbegin, rend, data, operator[], at, front, back, size, empty}

close #362
This commit is contained in:
Anarthal (Rubén Pérez)
2026-01-18 22:08:18 +01:00
committed by GitHub
parent 89e44dc017
commit 18ee72830b
10 changed files with 432 additions and 44 deletions

View File

@@ -122,7 +122,7 @@ auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
// The response must be consumed without suspending the
// coroutine i.e. without the use of async operations.
for (auto const& elem : resp.value().get_view())
for (auto const& elem : resp.value())
std::cout << elem.value << "\n";
std::cout << std::endl;

View File

@@ -133,7 +133,7 @@ auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
// The response must be consumed without suspending the
// coroutine i.e. without the use of async operations.
for (auto const& elem : resp.value().get_view())
for (auto const& elem : resp.value())
std::cout << elem.value << "\n";
std::cout << std::endl;

View File

@@ -72,7 +72,7 @@ auto receiver(std::shared_ptr<connection> conn) -> awaitable<void>
// The response must be consumed without suspending the
// coroutine i.e. without the use of async operations.
for (auto const& elem : resp.value().get_view())
for (auto const& elem : resp.value())
std::cout << elem.value << "\n";
std::cout << std::endl;

View File

@@ -72,7 +72,7 @@ auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
// The response must be consumed without suspending the
// coroutine i.e. without the use of async operations.
for (auto const& elem : resp.value().get_view())
for (auto const& elem : resp.value())
std::cout << elem.value << "\n";
std::cout << std::endl;

View File

@@ -11,10 +11,12 @@
#include <boost/redis/resp3/tree.hpp>
#include <boost/assert.hpp>
#include <boost/throw_exception.hpp>
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <stdexcept>
#include <string_view>
namespace boost::redis::resp3 {
@@ -228,15 +230,19 @@ void flat_tree::notify_done()
data_tmp_offset_ = data_.size;
}
const node_view& flat_tree::at(std::size_t i) const
{
if (i >= size())
BOOST_THROW_EXCEPTION(std::out_of_range("flat_tree::at"));
return view_tree_[i];
}
bool operator==(flat_tree const& a, flat_tree const& b)
{
// data is already taken into account by comparing the nodes.
// Only committed nodes should be taken into account.
auto a_nodes = a.get_view();
auto b_nodes = b.get_view();
return a_nodes.size() == b_nodes.size() &&
std::equal(a_nodes.begin(), a_nodes.end(), b_nodes.begin()) &&
a.total_msgs_ == b.total_msgs_;
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin()) &&
a.get_total_msgs() == b.get_total_msgs();
}
} // namespace boost::redis::resp3

View File

@@ -15,6 +15,7 @@
#include <boost/core/span.hpp>
#include <cstddef>
#include <iterator>
#include <memory>
namespace boost::redis {
@@ -45,8 +46,12 @@ struct flat_buffer {
* to obtain how many responses this object contains.
*
* Objects are typically created by the user and passed to @ref connection::async_exec
* to be used as response containers. Call @ref get_view to access the actual RESP3 nodes.
* Once populated, `flat_tree` can't be modified, except for @ref clear and assignment.
* to be used as response containers. Once populated, they can be used as a const range
* of @ref resp3::node_view objects. The usual random access range methods (like @ref at, @ref size or
* @ref front) are provided. Once populated, `flat_tree` can't be modified,
* except for @ref clear and assignment.
*
* `flat_tree` models `std::ranges::contiguous_range`.
*
* A `flat_tree` is conceptually similar to a pair of `std::vector` objects, one holding
* @ref resp3::node_view objects, and another owning the the string data that these views
@@ -54,6 +59,22 @@ struct flat_buffer {
*/
class flat_tree {
public:
/**
* @brief The type of the iterators returned by @ref begin and @ref end.
*
* It is guaranteed to be a contiguous iterator. While this is currently a pointer,
* users shouldn't rely on this fact, as the exact implementation may change between releases.
*/
using iterator = const node_view*;
/**
* @brief The type of the iterators returned by @ref rbegin and @ref rend.
*
* As with @ref iterator, users should treat this type as an unspecified
* contiguous iterator type rather than assuming a specific type.
*/
using reverse_iterator = std::reverse_iterator<iterator>;
/**
* @brief Default constructor.
*
@@ -70,7 +91,7 @@ public:
* Constructs a tree by taking ownership of the nodes in `other`.
*
* @par Object lifetimes
* References to the nodes and strings in `other` remain valid.
* Iterators, pointers and references to the nodes and strings in `other` remain valid.
*
* @par Exception safety
* No-throw guarantee.
@@ -95,8 +116,8 @@ public:
* `other` is left in a valid but unspecified state.
*
* @par Object lifetimes
* References to the nodes and strings in `other` remain valid.
* References to the nodes and strings in `*this` are invalidated.
* Iterators, pointers and references to the nodes and strings in `other` remain valid.
* Iterators, pointers and references to the nodes and strings in `*this` are invalidated.
*
* @par Exception safety
* No-throw guarantee.
@@ -110,16 +131,137 @@ public:
* After the copy, `*this` and `other` have independent lifetimes (usual copy semantics).
*
* @par Object lifetimes
* References to the nodes and strings in `*this` are invalidated.
* Iterators, pointers and references to the nodes and strings in `*this` are invalidated.
*
* @par Exception safety
* Basic guarantee. Memory allocations might throw.
*/
flat_tree& operator=(const flat_tree& other);
friend bool operator==(flat_tree const&, flat_tree const&);
/**
* @brief Returns an iterator to the first element of the node range.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns An iterator to the first node.
*/
iterator begin() const noexcept { return data(); }
friend bool operator!=(flat_tree const&, flat_tree const&);
/**
* @brief Returns an iterator past the last element in the node range.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns An iterator past the last element in the node range.
*/
iterator end() const noexcept { return data() + size(); }
/**
* @brief Returns an iterator to the first element of the reversed node range.
*
* Allows iterating the range of nodes in reverse order.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns An iterator to the first node of the reversed range.
*/
reverse_iterator rbegin() const noexcept { return reverse_iterator{end()}; }
/**
* @brief Returns an iterator past the last element of the reversed node range.
*
* Allows iterating the range of nodes in reverse order.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns An iterator past the last element of the reversed node range.
*/
reverse_iterator rend() const noexcept { return reverse_iterator{begin()}; }
/**
* @brief Returns a reference to the node at the specified position (checked access).
*
* @par Exception safety
* Strong guarantee. Throws `std::out_of_range` if `i >= size()`.
*
* @param i Position of the node to return.
* @returns A reference to the node at position `i`.
*/
const node_view& at(std::size_t i) const;
/**
* @brief Returns a reference to the node at the specified position (unchecked access).
*
* @par Precondition
* `i < size()`.
*
* @par Exception safety
* No-throw guarantee.
*
* @param i Position of the node to return.
* @returns A reference to the node at position `i`.
*/
const node_view& operator[](std::size_t i) const noexcept { return get_view()[i]; }
/**
* @brief Returns a reference to the first node.
*
* @par Precondition
* `!empty()`.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns A reference to the first node.
*/
const node_view& front() const noexcept { return get_view().front(); }
/**
* @brief Returns a reference to the last node.
*
* @par Precondition
* `!empty()`.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns A reference to the last node.
*/
const node_view& back() const noexcept { return get_view().back(); }
/**
* @brief Returns a pointer to the underlying node storage.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns A pointer to the underlying node array.
*/
const node_view* data() const noexcept { return view_tree_.data(); }
/**
* @brief Checks whether the tree is empty.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns `true` if the tree contains no nodes, `false` otherwise.
*/
bool empty() const noexcept { return size() == 0u; }
/**
* @brief Returns the number of nodes in the tree.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns The number of nodes.
*/
std::size_t size() const noexcept { return node_tmp_offset_; }
/** @brief Reserves capacity for incoming data.
*
@@ -142,7 +284,7 @@ public:
/** @brief Clears the tree so it contains no nodes.
*
* Calling this function removes every node, making
* @ref get_view return empty and @ref get_total_msgs
* the range contain no nodes, and @ref get_total_msgs
* return zero. It does not modify the object's capacity.
*
* To re-use a `flat_tree` for several requests,
@@ -189,17 +331,6 @@ public:
*/
auto data_capacity() const noexcept -> std::size_t { return data_.capacity; }
/** @brief Returns a vector with the nodes in the tree.
*
* This is the main way to access the contents of the tree.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns The nodes in the tree.
*/
span<const node_view> get_view() const noexcept { return {view_tree_.data(), node_tmp_offset_}; }
/** @brief Returns the number of memory reallocations that took place in the data buffer.
*
* This function returns how many reallocations in the data buffer were performed and
@@ -226,6 +357,7 @@ public:
private:
template <class> friend class adapter::detail::general_aggregate;
span<const node_view> get_view() const noexcept { return {data(), size()}; }
void notify_init();
void notify_done();

View File

@@ -205,7 +205,7 @@ BOOST_AUTO_TEST_CASE(exec_generic_flat_response)
BOOST_TEST_REQUIRE(finished);
BOOST_TEST(resp.has_value());
BOOST_TEST(resp->get_view().front().value == "PONG");
BOOST_TEST(resp.value().front().value == "PONG");
}
} // namespace

View File

@@ -682,15 +682,15 @@ struct test_pubsub_state_restoration_impl {
{
// Checks for the expected subscriptions and patterns after restoration
std::set<std::string_view> seen_channels, seen_patterns;
for (auto it = resp_push.get_view().begin(); it != resp_push.get_view().end();) {
for (auto it = resp_push.begin(); it != resp_push.end();) {
// The root element should be a push
BOOST_TEST_EQ(it->data_type, type::push);
BOOST_TEST_GE(it->aggregate_size, 2u);
BOOST_TEST(++it != resp_push.get_view().end());
BOOST_TEST(++it != resp_push.end());
// The next element should be the message type
std::string_view msg_type = it->value;
BOOST_TEST(++it != resp_push.get_view().end());
BOOST_TEST(++it != resp_push.end());
// The next element is the channel or pattern
if (msg_type == "subscribe")
@@ -699,7 +699,7 @@ struct test_pubsub_state_restoration_impl {
seen_patterns.insert(it->value);
// Skip the rest of the nodes
while (it != resp_push.get_view().end() && it->depth != 0u)
while (it != resp_push.end() && it->depth != 0u)
++it;
}

View File

@@ -11,19 +11,29 @@
#include <boost/redis/resp3/type.hpp>
#include <boost/assert/source_location.hpp>
#include <boost/config.hpp> // for a safe #include <version>
#include <boost/core/lightweight_test.hpp>
#include <boost/core/span.hpp>
#include "print_node.hpp"
#include <algorithm>
#include <array>
#include <initializer_list>
#include <iostream>
#include <iterator>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
#if (__cpp_lib_ranges >= 201911L) && (__cpp_lib_concepts >= 202002L)
#define BOOST_REDIS_TEST_RANGE_CONCEPTS
#include <ranges>
#endif
using boost::redis::adapter::adapt2;
using boost::redis::adapter::result;
using boost::redis::resp3::tree;
@@ -70,11 +80,7 @@ void check_nodes(
boost::span<const node_view> expected,
boost::source_location loc = BOOST_CURRENT_LOCATION)
{
if (!BOOST_TEST_ALL_EQ(
tree.get_view().begin(),
tree.get_view().end(),
expected.begin(),
expected.end()))
if (!BOOST_TEST_ALL_EQ(tree.begin(), tree.end(), expected.begin(), expected.end()))
std::cerr << "Called from " << loc << std::endl;
}
@@ -1090,6 +1096,233 @@ void test_move_assign_tmp()
BOOST_TEST_EQ(t.get_total_msgs(), 2u);
}
// --- Iterators ---
// We can obtain iterators using begin() and end() and use them to iterate
void test_iterators()
{
// Setup
flat_tree t;
add_nodes(t, "+node1\r\n");
add_nodes(t, ":200\r\n");
constexpr node_view node1{type::simple_string, 1u, 0u, "node1"};
constexpr node_view node2{type::number, 1u, 0u, "200"};
// These methods are const
const auto& tconst = t;
auto it = tconst.begin();
auto end = tconst.end();
// Iteration using iterators
BOOST_TEST_NE(it, end);
BOOST_TEST_EQ(*it, node1);
BOOST_TEST_NE(++it, end);
BOOST_TEST_EQ(*it, node2);
BOOST_TEST_EQ(++it, end);
// Iteration using range for
std::vector<node_view> nodes;
for (const auto& n : t)
nodes.push_back(n);
constexpr std::array expected_nodes{node1, node2};
BOOST_TEST_ALL_EQ(nodes.begin(), nodes.end(), expected_nodes.begin(), expected_nodes.end());
}
// Empty ranges don't cause trouble
void test_iterators_empty()
{
flat_tree t;
BOOST_TEST_EQ(t.begin(), t.end());
}
// Tmp area is not included in the range
// More or less tested with the add_nodes tests
void test_iterators_tmp()
{
parser p;
flat_tree t;
BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n"));
BOOST_TEST_EQ(t.begin(), t.end());
}
// The iterator should be contiguous
#ifdef BOOST_REDIS_TEST_RANGE_CONCEPTS
static_assert(std::contiguous_iterator<flat_tree::iterator>);
#endif
// --- Reverse iterators ---
// We can obtain iterators using rbegin() and rend() and use them to iterate
void test_reverse_iterators()
{
// Setup
flat_tree t;
add_nodes(t, "+node1\r\n");
add_nodes(t, ":200\r\n");
// These methods are const
const auto& tconst = t;
constexpr node_view expected_nodes[] = {
{type::number, 1u, 0u, "200" },
{type::simple_string, 1u, 0u, "node1"},
};
BOOST_TEST_ALL_EQ(
tconst.rbegin(),
tconst.rend(),
std::begin(expected_nodes),
std::end(expected_nodes));
}
// Empty ranges don't cause trouble
void test_reverse_iterators_empty()
{
flat_tree t;
BOOST_TEST(t.rbegin() == t.rend());
}
// Tmp area is not included in the range
void test_reverse_iterators_tmp()
{
parser p;
flat_tree t;
// Add one full message and a partial one
add_nodes(t, "*1\r\n+node1\r\n");
BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n"));
// Only the full message appears in the reversed range
constexpr node_view expected_nodes[] = {
{type::simple_string, 1u, 1u, "node1"},
{type::array, 1u, 0u, "" },
};
BOOST_TEST_ALL_EQ(t.rbegin(), t.rend(), std::begin(expected_nodes), std::end(expected_nodes));
}
// --- at ---
void test_at()
{
parser p;
flat_tree t;
// Add one full message and a partial one
add_nodes(t, "*1\r\n+node1\r\n");
BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n"));
// Nodes in the range can be accessed with at()
constexpr node_view n0{type::array, 1u, 0u, ""};
constexpr node_view n1{type::simple_string, 1u, 1u, "node1"};
BOOST_TEST_EQ(t.at(0u), n0);
BOOST_TEST_EQ(t.at(1u), n1);
// Nodes in the tmp area are not considered in range
BOOST_TEST_THROWS(t.at(2u), std::out_of_range);
BOOST_TEST_THROWS(t.at(3u), std::out_of_range);
// Indices out of range throw
BOOST_TEST_THROWS(t.at(4u), std::out_of_range);
BOOST_TEST_THROWS(t.at(5u), std::out_of_range);
BOOST_TEST_THROWS(t.at((std::numeric_limits<std::size_t>::max)()), std::out_of_range);
}
// Empty ranges don't cause trouble
void test_at_empty()
{
flat_tree t;
BOOST_TEST_THROWS(t.at(0u), std::out_of_range);
BOOST_TEST_THROWS(t.at(2u), std::out_of_range);
BOOST_TEST_THROWS(t.at((std::numeric_limits<std::size_t>::max)()), std::out_of_range);
}
// --- operator[], front, back ---
void test_unchecked_access()
{
flat_tree t;
add_nodes(t, "*2\r\n+node1\r\n+node2\r\n");
constexpr node_view n0{type::array, 2u, 0u, ""};
constexpr node_view n1{type::simple_string, 1u, 1u, "node1"};
constexpr node_view n2{type::simple_string, 1u, 1u, "node2"};
// operator []
BOOST_TEST_EQ(t[0u], n0);
BOOST_TEST_EQ(t[1u], n1);
BOOST_TEST_EQ(t[2u], n2);
// Front and back
BOOST_TEST_EQ(t.front(), n0);
BOOST_TEST_EQ(t.back(), n2);
}
// --- data ---
void test_data()
{
flat_tree t;
add_nodes(t, "*1\r\n+node1\r\n");
constexpr node_view expected_nodes[] = {
{type::array, 1u, 0u, "" },
{type::simple_string, 1u, 1u, "node1"},
};
BOOST_TEST_NE(t.data(), nullptr);
BOOST_TEST_ALL_EQ(t.data(), t.data() + 2u, std::begin(expected_nodes), std::end(expected_nodes));
}
// Empty ranges don't cause trouble
void test_data_empty()
{
flat_tree t;
BOOST_TEST_EQ(t.data(), nullptr);
}
// --- size and empty ---
void test_size()
{
flat_tree t;
add_nodes(t, "*1\r\n+node1\r\n");
BOOST_TEST_EQ(t.size(), 2u);
BOOST_TEST_NOT(t.empty());
}
void test_size_empty()
{
flat_tree t;
BOOST_TEST_EQ(t.size(), 0u);
BOOST_TEST(t.empty());
}
// Tmp area not taken into account
void test_size_tmp()
{
parser p;
flat_tree t;
// Add one full message and a partial one
add_nodes(t, "*1\r\n+node1\r\n");
BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n"));
BOOST_TEST_EQ(t.size(), 2u);
BOOST_TEST_NOT(t.empty());
}
void test_size_tmp_only()
{
parser p;
flat_tree t;
// Add one partial message
BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n"));
BOOST_TEST_EQ(t.size(), 0u);
BOOST_TEST(t.empty());
}
// The range should model contiguous range
#ifdef BOOST_REDIS_TEST_RANGE_CONCEPTS
static_assert(std::ranges::contiguous_range<flat_tree>);
#endif
// --- Comparison ---
void test_comparison_different()
{
@@ -1310,6 +1543,27 @@ int main()
test_move_assign_both_empty();
test_move_assign_tmp();
test_iterators();
test_iterators_empty();
test_iterators_tmp();
test_reverse_iterators();
test_reverse_iterators_empty();
test_reverse_iterators_tmp();
test_at();
test_at_empty();
test_unchecked_access();
test_data();
test_data_empty();
test_size();
test_size_empty();
test_size_tmp();
test_size_tmp_only();
test_comparison_different();
test_comparison_different_node_types();
test_comparison_equal();

View File

@@ -36,11 +36,7 @@ void test_success()
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, "hello"},
};
BOOST_TEST_ALL_EQ(
resp->get_view().begin(),
resp->get_view().end(),
expected_nodes.begin(),
expected_nodes.end());
BOOST_TEST_ALL_EQ(resp->begin(), resp->end(), expected_nodes.begin(), expected_nodes.end());
}
// If an error of any kind appears, we set the overall result to error