--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
+// vim: ts=8 sw=2 sts=2 expandtab ft=cpp
+
+#ifndef CEPH_COMMON_CONTAINER_CONCEPTS_H
+ #define CEPH_COMMON_CONTAINER_CONCEPTS_H
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+ */
+
+#include <ranges>
+#include <cstddef>
+#include <utility>
+#include <concepts>
+#include <iterator>
+#include <algorithm>
+#include <type_traits>
+
+/* A helpful collection of C++ Concepts that aren't available in the C++23
+* standard. The goal here is to provide common and practical concepts and
+* capability queries.
+*
+* Some of the material is derived from:
+* - https://eel.is/c++draft/range.utility.conv
+* - https://en.cppreference.com/w/cpp/ranges/to
+*
+* The facilities provided by this mini-library include:
+* - some Concepts not in C++ (such as associative_container and
+* unordered_associative_container);
+* - a local name for some exposition-only C++ concepts used by the standard (such as
+* container_compatible_range);
+*
+* - capability query predicates, which make metaprogramming tasks more straightforward
+* and are very useful in a variety of algorithms;
+*
+* - end()/begin() style function forms for some container operations (like clear());
+*
+* - utility functions for capturing common cases like optionally clearing a container
+* based on whether or not the operation is supported by its concrete type;
+*
+* - append helpers that construct values through the container's natural
+* insertion operation, including stateful appenders for forward-only containers;
+*
+* - range materialization helpers for building containers without requiring
+* std::ranges::to support from the local standard library;
+*
+* Note that a number of the things you might wish were in here are already in implemented
+* in the Standard Library-- as a guide (depending on your mental model!) this may be helpful:
+* - ceph::concepts::sequence -> std::ranges::input_range
+* - ceph::concepts::contiguous_sequence -> std::ranges::contiguous_range
+* - ceph::util::erase_if -> std::erase_if
+* - ceph::util::size -> std::ranges::size
+* - ceph::util::empty -> std::ranges::empty
+*
+* Capability predicates such as can_erase_if and has_size are not direct
+* operation wrappers; keep them when they make generic constraints clearer.
+*
+* This header also keeps local names for useful C++23 exposition-only
+* constraints from ranges::to, including reservable_container and
+* container_appendable.
+*/
+
+// General Concepts:
+namespace ceph::concepts {
+
+template <typename T, typename ...Ts>
+concept same_as_any = (std::same_as<T, Ts> || ...);
+
+template <typename ContainerT>
+concept associative_container = requires(ContainerT& c, const ContainerT& cc,
+ const typename ContainerT::key_type& key) {
+ requires std::ranges::range<ContainerT>;
+
+ typename ContainerT::key_type;
+ typename ContainerT::value_type;
+ typename ContainerT::key_compare;
+ typename ContainerT::value_compare;
+ typename ContainerT::iterator;
+ typename ContainerT::const_iterator;
+ typename ContainerT::size_type;
+
+ { cc.key_comp() } -> std::same_as<typename ContainerT::key_compare>;
+ { cc.value_comp() } -> std::same_as<typename ContainerT::value_compare>;
+ { c.find(key) } -> std::same_as<typename ContainerT::iterator>;
+ { cc.find(key) } -> std::same_as<typename ContainerT::const_iterator>;
+ { c.count(key) } -> std::convertible_to<typename ContainerT::size_type>;
+ { c.lower_bound(key) } -> std::same_as<typename ContainerT::iterator>;
+ { cc.lower_bound(key) } -> std::same_as<typename ContainerT::const_iterator>;
+ { c.upper_bound(key) } -> std::same_as<typename ContainerT::iterator>;
+ { cc.upper_bound(key) } -> std::same_as<typename ContainerT::const_iterator>;
+ { c.equal_range(key) };
+ { cc.equal_range(key) };
+};
+
+template <typename ContainerT>
+concept unordered_associative_container = requires(ContainerT& c, const ContainerT& cc,
+ const typename ContainerT::key_type& key) {
+ requires std::ranges::range<ContainerT>;
+
+ typename ContainerT::key_type;
+ typename ContainerT::value_type;
+ typename ContainerT::hasher;
+ typename ContainerT::key_equal;
+ typename ContainerT::iterator;
+ typename ContainerT::const_iterator;
+ typename ContainerT::size_type;
+
+ { cc.hash_function() } -> std::same_as<typename ContainerT::hasher>;
+ { cc.key_eq() } -> std::same_as<typename ContainerT::key_equal>;
+ { c.find(key) } -> std::same_as<typename ContainerT::iterator>;
+ { cc.find(key) } -> std::same_as<typename ContainerT::const_iterator>;
+ { c.count(key) } -> std::convertible_to<typename ContainerT::size_type>;
+ { c.equal_range(key) };
+ { cc.equal_range(key) };
+ { c.bucket_count() } -> std::convertible_to<typename ContainerT::size_type>;
+};
+
+template <typename RangeT, typename T>
+concept container_compatible_range =
+ std::ranges::input_range<RangeT> && std::convertible_to<std::ranges::range_reference_t<RangeT>, T>;
+
+template <typename ContainerT>
+concept reservable_container =
+ std::ranges::sized_range<ContainerT> &&
+ requires(ContainerT& c, std::ranges::range_size_t<ContainerT> n) {
+ c.reserve(n);
+ { c.capacity() } -> std::same_as<decltype(n)>;
+ { c.max_size() } -> std::same_as<decltype(n)>;
+ };
+
+template <typename ContainerT, typename RefT>
+concept container_appendable = requires(ContainerT& c, RefT&& ref) {
+ requires
+ requires { c.emplace_back(std::forward<RefT>(ref)); } ||
+ requires { c.push_back(std::forward<RefT>(ref)); } ||
+ requires { { c.emplace(c.end(), std::forward<RefT>(ref)) } -> std::same_as<decltype(c.end())>; } ||
+ requires { c.insert(c.end(), std::forward<RefT>(ref)); };
+};
+
+} // namespace ceph::concepts
+
+// Capability Queries:
+namespace ceph::concepts {
+
+template <typename ContainerT, typename... ArgsT>
+concept has_emplace_back = requires(ContainerT& c, ArgsT&&... args) {
+ c.emplace_back(std::forward<ArgsT>(args)...);
+};
+
+template <typename ContainerT, typename T>
+concept has_push_back = requires(ContainerT& c, T&& v) {
+ c.push_back(std::forward<T>(v));
+};
+
+template <typename ContainerT, typename... ArgsT>
+concept has_emplace_append = requires(ContainerT& c, ArgsT&&... args) {
+ { c.emplace(std::end(c), std::forward<ArgsT>(args)...) } ->
+ std::same_as<typename ContainerT::iterator>;
+};
+
+template <typename ContainerT, typename T>
+concept has_insert_append = requires(ContainerT& c, T&& v) {
+ c.insert(std::end(c), std::forward<T>(v));
+};
+
+template <typename ContainerT, typename IteratorT, typename... ArgsT>
+concept has_emplace_after = requires(ContainerT& c, IteratorT pos, ArgsT&&... args) {
+ c.emplace_after(pos, std::forward<ArgsT>(args)...);
+};
+
+template <typename ContainerT, typename IteratorT, typename T>
+concept has_insert_after = requires(ContainerT& c, IteratorT pos, T&& v) {
+ c.insert_after(pos, std::forward<T>(v));
+};
+
+template <typename ContainerT, typename RangeT>
+concept has_append_range = requires(ContainerT& c, RangeT&& range) {
+ c.append_range(std::forward<RangeT>(range));
+};
+
+template <typename ContainerT, typename RangeT>
+concept has_insert_range_append = requires(ContainerT& c, RangeT&& range) {
+ c.insert_range(std::end(c), std::forward<RangeT>(range));
+};
+
+template <typename ContainerT, typename RangeT>
+concept has_insert_iterator_range_append = requires(ContainerT& c, RangeT&& range) {
+ requires std::ranges::input_range<RangeT>;
+
+ c.insert(std::end(c), std::begin(range), std::end(range));
+};
+
+template <typename ContainerT, typename IteratorT, typename RangeT>
+concept has_insert_range = requires(ContainerT& c, IteratorT pos, RangeT&& range) {
+ c.insert_range(pos, std::forward<RangeT>(range));
+};
+
+template <typename ContainerT, typename IteratorT, typename RangeT>
+concept has_insert_iterator_range = requires(ContainerT& c, IteratorT pos, RangeT&& range) {
+ requires std::ranges::input_range<RangeT>;
+
+ c.insert(pos, std::begin(range), std::end(range));
+};
+
+template <typename ContainerT, typename T>
+concept has_emplace_front = requires(ContainerT& c, T&& v) {
+ c.emplace_front(std::forward<T>(v));
+};
+
+template <typename ContainerT, typename T>
+concept has_push_front = requires(ContainerT& c, T&& v) {
+ c.push_front(std::forward<T>(v));
+};
+
+template <typename ContainerT, typename T>
+concept has_emplace_at_begin = requires(ContainerT& c, T&& v) {
+ { c.emplace(std::begin(c), std::forward<T>(v)) } -> std::same_as<typename ContainerT::iterator>;
+};
+
+template <typename ContainerT, typename T>
+concept has_insert_at_begin = requires(ContainerT& c, T&& v) {
+ c.insert(std::begin(c), std::forward<T>(v));
+};
+
+template <typename ContainerT>
+concept has_pop_front = requires(ContainerT& c) {
+ c.pop_front();
+};
+
+template <typename ContainerT>
+concept has_erase_begin = requires(ContainerT& c) {
+ c.erase(std::begin(c));
+};
+
+template <typename ContainerT, typename ArgT>
+concept has_erase = requires(ContainerT& c, ArgT&& v) {
+ c.erase(std::forward<ArgT>(v));
+};
+
+template <typename ContainerT, typename IteratorT, typename SentinelT>
+concept has_erase_range = requires(ContainerT& c, IteratorT first, SentinelT last) {
+ c.erase(first, last);
+};
+
+template <typename ContainerT, typename PredicateT>
+concept has_remove_if = requires(ContainerT& c, PredicateT pred) {
+ c.remove_if(pred);
+};
+
+template <typename ContainerT>
+concept has_clear = requires(ContainerT& c) {
+ c.clear();
+};
+
+template <typename ContainerT>
+concept has_reserve = requires(ContainerT& c, std::size_t n) {
+ c.reserve(n);
+};
+
+template <typename ContainerT>
+concept has_capacity = requires(const ContainerT& c) {
+ { c.capacity() } -> std::convertible_to<std::size_t>;
+};
+
+template <typename ContainerT>
+concept has_max_size = requires(const ContainerT& c) {
+ { c.max_size() } -> std::convertible_to<std::size_t>;
+};
+
+template <typename ContainerT>
+concept has_size = requires(const ContainerT& c) {
+ { c.size() } -> std::convertible_to<std::size_t>;
+};
+
+template <typename ContainerT>
+concept has_empty = requires(const ContainerT& c) {
+ { c.empty() } -> std::convertible_to<bool>;
+};
+
+template <typename ContainerT>
+concept has_resize = requires(ContainerT& c, std::size_t n) {
+ c.resize(n);
+};
+
+} // namespace ceph::concepts
+
+// Helpers for composing aggregate capability checks:
+namespace ceph::concepts::detail {
+
+template <typename ContainerT, typename RangeT>
+concept can_append_with_insert =
+ ceph::concepts::has_insert_range_append<ContainerT, RangeT> || ceph::concepts::has_insert_iterator_range_append<ContainerT, RangeT>;
+
+} // namespace ceph::concepts::detail
+
+// Aggregate Capability Checks:
+namespace ceph::concepts {
+
+template <typename ContainerT, typename T>
+concept can_append =
+ container_appendable<ContainerT, T>;
+
+template <typename ContainerT, typename... ArgsT>
+concept can_emplace_append =
+ has_emplace_back<ContainerT, ArgsT...> ||
+ has_emplace_append<ContainerT, ArgsT...> ||
+ (std::constructible_from<typename ContainerT::value_type, ArgsT...> &&
+ can_append<ContainerT, typename ContainerT::value_type>);
+
+template <typename ContainerT, typename IteratorT, typename T>
+concept can_insert_after =
+ has_emplace_after<ContainerT, IteratorT, T> ||
+ has_insert_after<ContainerT, IteratorT, T>;
+
+template <typename ContainerT, typename IteratorT, typename... ArgsT>
+concept can_emplace_after =
+ has_emplace_after<ContainerT, IteratorT, ArgsT...> ||
+ (std::constructible_from<typename ContainerT::value_type, ArgsT...> &&
+ can_insert_after<ContainerT, IteratorT, typename ContainerT::value_type>);
+
+template <typename ContainerT, typename RangeT>
+concept can_append_range =
+ std::ranges::input_range<RangeT> &&
+ (has_append_range<ContainerT, RangeT> ||
+ detail::can_append_with_insert<ContainerT, RangeT> ||
+ can_emplace_append<ContainerT, std::ranges::range_reference_t<RangeT>>);
+
+template <typename ContainerT, typename IteratorT, typename RangeT>
+concept can_insert_any_range =
+ has_insert_range<ContainerT, IteratorT, RangeT> || has_insert_iterator_range<ContainerT, IteratorT, RangeT>;
+
+template <typename ContainerT, typename T>
+concept can_prepend =
+ has_emplace_front<ContainerT, T> ||
+ has_push_front<ContainerT, T> ||
+ has_emplace_at_begin<ContainerT, T> ||
+ has_insert_at_begin<ContainerT, T>;
+
+template <typename ContainerT, typename PredicateT>
+concept can_erase_remove_if = requires(ContainerT& c, PredicateT pred) {
+ requires std::ranges::common_range<ContainerT>;
+ requires has_erase_range<ContainerT,
+ std::ranges::iterator_t<ContainerT>,
+ std::ranges::sentinel_t<ContainerT>>;
+
+ std::remove_if(std::begin(c), std::end(c), pred);
+};
+
+template <typename ContainerT, typename PredicateT>
+concept can_erase_if =
+ has_remove_if<ContainerT, PredicateT> || can_erase_remove_if<ContainerT, PredicateT>;
+
+template <typename ContainerT>
+concept can_remove_front =
+ has_pop_front<ContainerT> || has_erase_begin<ContainerT>;
+
+} // namespace ceph::concepts
+
+// Helpers:
+// - most of these try to "do the right thing" with the underlying container
+// as-appropriate; use some caution in performance-sensitive situations (although
+// the answer may be far from cut and dried as cache locality and other features
+// of modern CPUs can produce extremely counterintuitive results vis-a-vis O(n),
+// n may need to be surprisingly large before inserting at the head of a list is
+// *actually* faster than shifting an array, for instance):
+namespace ceph::util {
+
+template <typename ContainerT, typename T>
+requires ceph::concepts::can_append<ContainerT, T>
+constexpr void push_back(ContainerT& c, T&& v)
+{
+ if constexpr (ceph::concepts::has_emplace_back<ContainerT, T>) {
+ c.emplace_back(std::forward<T>(v));
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_push_back<ContainerT, T>) {
+ c.push_back(std::forward<T>(v));
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_emplace_append<ContainerT, T>) {
+ c.emplace(std::end(c), std::forward<T>(v));
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_insert_append<ContainerT, T>) {
+ c.insert(std::end(c), std::forward<T>(v));
+ return;
+ }
+}
+
+// Note: emplace_back() can return a reference, hence decltype(auto):
+template <typename ContainerT, typename... ArgsT>
+constexpr decltype(auto) emplace_back(ContainerT& c, ArgsT&&... args)
+{
+ return c.emplace_back(std::forward<ArgsT>(args)...);
+}
+
+template <typename ContainerT, typename... ArgsT>
+requires ceph::concepts::can_emplace_append<ContainerT, ArgsT...>
+constexpr void emplace_append(ContainerT& c, ArgsT&&... args)
+{
+ if constexpr (ceph::concepts::has_emplace_back<ContainerT, ArgsT...>) {
+ c.emplace_back(std::forward<ArgsT>(args)...);
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_emplace_append<ContainerT, ArgsT...>) {
+ c.emplace(std::end(c), std::forward<ArgsT>(args)...);
+ return;
+ }
+
+ if constexpr (std::constructible_from<typename ContainerT::value_type, ArgsT...>) {
+ push_back(c, typename ContainerT::value_type(std::forward<ArgsT>(args)...));
+ }
+}
+
+template <typename ContainerT, typename RangeT>
+requires ceph::concepts::can_append_range<ContainerT, RangeT>
+constexpr void append_range(ContainerT& c, RangeT&& range)
+{
+ if constexpr (ceph::concepts::has_append_range<ContainerT, RangeT>) {
+ c.append_range(std::forward<RangeT>(range));
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_insert_range_append<ContainerT, RangeT>) {
+ c.insert_range(std::end(c), std::forward<RangeT>(range));
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_insert_iterator_range_append<ContainerT, RangeT>) {
+ c.insert(std::end(c), std::begin(range), std::end(range));
+ return;
+ }
+
+ if constexpr (ceph::concepts::can_emplace_append<ContainerT, std::ranges::range_reference_t<RangeT>>) {
+ for (auto&& v : range) {
+ emplace_append(c, std::forward<decltype(v)>(v));
+ }
+ return;
+ }
+}
+
+template <typename ContainerT, typename IteratorT, typename RangeT>
+requires ceph::concepts::can_insert_any_range<ContainerT, IteratorT, RangeT>
+constexpr void insert_range(ContainerT& c, IteratorT pos, RangeT&& range)
+{
+ if constexpr (ceph::concepts::has_insert_range<ContainerT, IteratorT, RangeT>) {
+ c.insert_range(pos, std::forward<RangeT>(range));
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_insert_iterator_range<ContainerT, IteratorT, RangeT>) {
+ c.insert(pos, std::begin(range), std::end(range));
+ return;
+ }
+}
+
+template <typename ContainerT, typename IteratorT, typename T>
+requires ceph::concepts::can_insert_after<ContainerT, IteratorT, T>
+constexpr auto insert_after(ContainerT& c, IteratorT pos, T&& v)
+{
+ if constexpr (ceph::concepts::has_emplace_after<ContainerT, IteratorT, T>) {
+ return c.emplace_after(pos, std::forward<T>(v));
+ }
+
+ if constexpr (ceph::concepts::has_insert_after<ContainerT, IteratorT, T>) {
+ return c.insert_after(pos, std::forward<T>(v));
+ }
+}
+
+template <typename ContainerT, typename IteratorT, typename... ArgsT>
+requires ceph::concepts::can_emplace_after<ContainerT, IteratorT, ArgsT...>
+constexpr auto emplace_after(ContainerT& c, IteratorT pos, ArgsT&&... args)
+{
+ if constexpr (ceph::concepts::has_emplace_after<ContainerT, IteratorT, ArgsT...>) {
+ return c.emplace_after(pos, std::forward<ArgsT>(args)...);
+ }
+
+ if constexpr (std::constructible_from<typename ContainerT::value_type, ArgsT...>) {
+ return insert_after(c, pos, typename ContainerT::value_type(std::forward<ArgsT>(args)...));
+ }
+}
+
+// Fills a role similar to back_inserter(), but calling emplace_append().
+template <typename ContainerT>
+struct container_appender final {
+ ContainerT& container;
+
+ public:
+ explicit constexpr container_appender(ContainerT& container)
+ : container(container) {}
+
+ public:
+ template <typename... ArgsT>
+ requires ceph::concepts::can_emplace_append<ContainerT, ArgsT...>
+ constexpr void emplace(ArgsT&&... args)
+ {
+ emplace_append(container, std::forward<ArgsT>(args)...);
+ }
+};
+
+template <typename ContainerT>
+struct emplace_after_appender final {
+ ContainerT& container;
+ std::ranges::iterator_t<ContainerT> pos;
+
+ public:
+ explicit constexpr emplace_after_appender(ContainerT& container)
+ : container(container),
+ pos(container.before_begin()) {}
+
+ public:
+ template <typename... ArgsT>
+ requires ceph::concepts::can_emplace_after<ContainerT,
+ std::ranges::iterator_t<ContainerT>,
+ ArgsT...>
+ constexpr void emplace(ArgsT&&... args)
+ {
+ pos = emplace_after(container, pos, std::forward<ArgsT>(args)...);
+ }
+};
+
+template <typename ContainerT>
+constexpr auto make_appender(ContainerT& c)
+{
+ constexpr auto inserts_after =
+ std::ranges::forward_range<ContainerT> &&
+ !std::ranges::bidirectional_range<ContainerT> &&
+ requires (ContainerT& container) {
+ container.before_begin();
+ };
+
+ if constexpr (inserts_after) {
+ return emplace_after_appender<ContainerT> { c };
+ }
+
+ if constexpr (!inserts_after) {
+ return container_appender<ContainerT> { c };
+ }
+}
+
+template <typename ContainerT, typename ArgT>
+constexpr auto erase(ContainerT& c, ArgT&& v)
+{
+ return c.erase(std::forward<ArgT>(v));
+}
+
+template <typename ContainerT, typename IteratorT, typename SentinelT>
+constexpr auto erase(ContainerT& c, IteratorT first, SentinelT last)
+{
+ return c.erase(first, last);
+}
+
+template <typename ContainerT, typename T>
+requires ceph::concepts::can_prepend<ContainerT, T>
+constexpr void push_front(ContainerT& c, T&& v)
+{
+ if constexpr (ceph::concepts::has_emplace_front<ContainerT, T>) {
+ c.emplace_front(std::forward<T>(v));
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_push_front<ContainerT, T>) {
+ c.push_front(std::forward<T>(v));
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_emplace_at_begin<ContainerT, T>) {
+ c.emplace(std::begin(c), std::forward<T>(v));
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_insert_at_begin<ContainerT, T>) {
+ c.insert(std::begin(c), std::forward<T>(v));
+ return;
+ }
+}
+
+template <typename ContainerT>
+requires ceph::concepts::can_remove_front<ContainerT>
+constexpr void pop_front(ContainerT& c)
+{
+ if constexpr (ceph::concepts::has_pop_front<ContainerT>) {
+ c.pop_front();
+ return;
+ }
+
+ if constexpr (ceph::concepts::has_erase_begin<ContainerT>) {
+ c.erase(std::begin(c));
+ return;
+ }
+}
+
+template <typename ContainerT>
+constexpr std::size_t capacity(const ContainerT& c)
+{
+ return c.capacity();
+}
+
+template <typename ContainerT>
+constexpr std::size_t max_size(const ContainerT& c)
+{
+ return c.max_size();
+}
+
+template <typename ContainerT>
+constexpr void resize(ContainerT& c, std::size_t n)
+{
+ c.resize(n);
+}
+
+template <typename ContainerT>
+constexpr void clear(ContainerT& c)
+{
+ c.clear();
+}
+
+// It's often handy to be able to easily resize if possible,
+// but continue along either way-- so, here we are:
+template <typename ContainerT>
+constexpr void maybe_resize(ContainerT& c, std::size_t n)
+{
+ if constexpr (ceph::concepts::has_resize<ContainerT>) {
+ c.resize(n);
+ }
+}
+
+// reserve() also is frequently "nice to have" in some algorithms:
+template <typename ContainerT>
+constexpr void maybe_reserve(ContainerT& c, std::size_t n)
+{
+ if constexpr (ceph::concepts::has_reserve<ContainerT>) {
+ c.reserve(n);
+ }
+}
+
+template <typename ContainerT, std::ranges::input_range RangeT>
+requires ceph::concepts::can_append_range<ContainerT, RangeT>
+constexpr ContainerT collect_as(RangeT&& range)
+{
+ ContainerT out;
+
+ if constexpr (std::ranges::sized_range<RangeT>) {
+ maybe_reserve(out, std::ranges::size(range));
+ }
+
+ append_range(out, std::forward<RangeT>(range));
+
+ return out;
+}
+
+} // namespace ceph::util
+
+#endif
/* Read a key range into an STL associative container. */
std::map<std::string, std::string> people;
-lfdb::get(dbh,
- lfdb::select { "person/" },
- std::inserter(people, std::end(people)));
+lfdb::get(dbh, lfdb::select { "person/" }, people);
```
## Key Ordering
#include <filesystem>
#include <type_traits>
+#include "common/container_concepts.h"
+
#ifdef __cpp_lib_flat_map
#include <flat_map>
template <typename ...Args>
} // namespace ceph::libfdb
-// MOAR forward declarations-- "pay no attention to that man behind the curtain":
-namespace ceph::libfdb::detail {
-
-template <typename T, typename ...Ts>
-concept is_any_of = (std::is_same_v<T, Ts> || ...);
-
-struct future_value;
-
-template <typename ValueT = std::string>
-std::pair<std::string, ValueT> to_decoded_kv_pair(const FDBKeyValue& kv);
-
-inline fdb_error_t do_commit(transaction_handle& txn);
-
-inline void transaction_set_kv_bytes(const transaction_handle& txn,
- std::span<const std::uint8_t> k,
- std::span<const std::uint8_t> v);
-
-inline future_value block_until_ready(future_value&& fv);
-inline fdb_error_t get_future_error(const future_value& fv);
-inline future_value wait_for_on_error(FDBTransaction* txn, fdb_error_t original_error);
-inline future_value get_range_future_from_transaction(ceph::libfdb::transaction& txn, const ceph::libfdb::select& selection, int iteration);
-
-// A generator that produces successive spans for a range:
-inline std::generator<std::span<const FDBKeyValue>> generate_FDB_pairs(ceph::libfdb::transaction& txn, ceph::libfdb::select key_range);
-
-// Stores a successively-generated of kv pair results to an iterator:
-template <typename OutIterT>
-requires std::output_iterator<OutIterT, std::pair<std::string, std::string>>
-inline bool get_value_range_from_transaction(ceph::libfdb::transaction& txn, const ceph::libfdb::select& key_range, OutIterT out_iter);
-
-} // namespace ceph::libfdb::detail
-
namespace ceph::libfdb::concepts {
// Note that "stringlikes" are not all "stringview-likes", such as when they can be
kv.second;
};
+template <typename OutIterT>
+concept string_key_value_output_iterator =
+ std::output_iterator<OutIterT, std::pair<std::string, std::string>>;
+
+template <typename OutContainerT>
+concept string_key_value_output_container =
+ ceph::concepts::can_append<OutContainerT, std::pair<std::string, std::string>>;
+
template <typename FnT>
concept value_callback =
std::invocable<FnT&, std::span<const std::uint8_t>>;
// There's a high likelihood that we're going to get more sophisticated selectors,
// so this is doing a more important job than it may appear to be:
template <typename T>
-concept selector = ceph::libfdb::detail::is_any_of<T, ceph::libfdb::select>;
+concept selector = ceph::concepts::same_as_any<T, ceph::libfdb::select>;
} // namespace ceph::libfdb::concepts
+// MOAR forward declarations-- "pay no attention to that man behind the curtain":
+namespace ceph::libfdb::detail {
+
+struct future_value;
+
+template <typename ValueT = std::string>
+std::pair<std::string, ValueT> to_decoded_kv_pair(const FDBKeyValue& kv);
+
+inline fdb_error_t do_commit(transaction_handle& txn);
+
+inline void transaction_set_kv_bytes(const transaction_handle& txn,
+ std::span<const std::uint8_t> k,
+ std::span<const std::uint8_t> v);
+
+inline future_value block_until_ready(future_value&& fv);
+inline fdb_error_t get_future_error(const future_value& fv);
+inline future_value wait_for_on_error(FDBTransaction* txn, fdb_error_t original_error);
+inline future_value get_range_future_from_transaction(ceph::libfdb::transaction& txn, const ceph::libfdb::select& selection, int iteration);
+
+// A generator that produces successive spans for a range:
+inline std::generator<std::span<const FDBKeyValue>> generate_FDB_pairs(ceph::libfdb::transaction& txn, ceph::libfdb::select key_range);
+
+// Stores a successively-generated of kv pair results to an iterator:
+template <typename OutIterT>
+requires ceph::libfdb::concepts::string_key_value_output_iterator<OutIterT>
+inline bool get_value_range_from_transaction(ceph::libfdb::transaction& txn, const ceph::libfdb::select& key_range, OutIterT out_iter);
+
+} // namespace ceph::libfdb::detail
+
// libfdb_exception: How to deal, when Bad Things(TM) happen:
namespace ceph::libfdb {
// JFW: it's not as easy to wedge an output_range into here as it appears, perhaps
// needs to be revisited; I'm binding it to what's actually used in practice for now:
template <typename OutIterT>
- requires std::output_iterator<OutIterT, std::pair<std::string, std::string>>
+ requires concepts::string_key_value_output_iterator<OutIterT>
bool get(const ceph::libfdb::select& key_range, OutIterT out_iter) {
return ceph::libfdb::detail::get_value_range_from_transaction(*this, key_range, out_iter);
}
std::string_view,
OutputTargetOrFnT&&,
const commit_after_op);
- friend inline bool get(ceph::libfdb::transaction_handle, const ceph::libfdb::select&, auto, const commit_after_op);
+ friend inline bool get(ceph::libfdb::transaction_handle,
+ const ceph::libfdb::select&,
+ concepts::string_key_value_output_iterator auto,
+ const commit_after_op);
friend inline void erase(ceph::libfdb::transaction_handle, std::string_view, const commit_after_op);
friend inline void erase(ceph::libfdb::transaction_handle, const ceph::libfdb::select&, const commit_after_op);
template <typename ValueT, typename AssocT>
inline AssocT collect_pairs(std::span<const FDBKeyValue> pairs)
{
- AssocT out;
- std::ranges::copy(decode_pairs<ValueT>(pairs), std::inserter(out, std::end(out)));
- return out;
+ return ceph::util::collect_as<AssocT>(decode_pairs<ValueT>(pairs));
}
template <typename AssocT>
}
template <typename OutIterT>
-requires std::output_iterator<OutIterT, std::pair<std::string, std::string>>
+requires concepts::string_key_value_output_iterator<OutIterT>
inline bool get_value_range_from_transaction(transaction& txn, const select& key_range, OutIterT out_iter)
{
auto flattened = detail::generate_FDB_pairs(txn, key_range) | std::views::join;
}
// Gather the flattened list into *overlapping* libfdb::select pairs:
- return std::views::iota(std::size_t{0}, xs.size() - 1)
- | std::views::transform([&parent, xs](const auto i) {
- const auto& fst = xs[i];
- const auto& snd = xs[i + 1];
- const auto first_key = std::string_view((const char *)fst.key,
- static_cast<std::string::size_type>(fst.key_length));
- const auto second_key = std::string_view((const char *)snd.key,
- static_cast<std::string::size_type>(snd.key_length));
-
- ceph::libfdb::select split(first_key, second_key);
-
- split.options = parent.options;
- split.begin_inclusive = (0 == i) ? parent.begin_inclusive : true;
- split.end_inclusive = (i + 2 == xs.size()) ? parent.end_inclusive : false;
- return split;
- })
- | std::ranges::to<std::vector<ceph::libfdb::select>>();
+ return ceph::util::collect_as<std::vector<ceph::libfdb::select>>(
+ std::views::iota(std::size_t{0}, xs.size() - 1)
+ | std::views::transform([&parent, xs](const auto i) {
+ const auto& fst = xs[i];
+ const auto& snd = xs[i + 1];
+ const auto first_key = std::string_view((const char *)fst.key,
+ static_cast<std::string::size_type>(fst.key_length));
+ const auto second_key = std::string_view((const char *)snd.key,
+ static_cast<std::string::size_type>(snd.key_length));
+
+ ceph::libfdb::select split(first_key, second_key);
+
+ split.options = parent.options;
+ split.begin_inclusive = (0 == i) ? parent.begin_inclusive : true;
+ split.end_inclusive = (i + 2 == xs.size()) ? parent.end_inclusive : false;
+ return split;
+ }));
}
// Finding a clear example both in the samples and in the documentation is not very easy. The
// statelessness of FDB requests bleeds into here with basically no hand-holding, but note for instance
// JFW: Satisfying output_iterator is not as straightforward as it appears, I need to look at this mechanism again; meanwhile, the template doesn't
// /prevent/ future type narrowing, but I'm forcing it to std::string for now:
inline bool get(ceph::libfdb::transaction_handle txn,
- const ceph::libfdb::select& key_range, auto out_iter,
+ const ceph::libfdb::select& key_range,
+ concepts::string_key_value_output_iterator auto out_iter,
const ceph::libfdb::commit_after_op commit_after)
{
return detail::commit_noreplay(txn, commit_after,
}
inline bool get(ceph::libfdb::transaction_handle txn,
- const ceph::libfdb::select& key_range, auto out_iter)
+ const ceph::libfdb::select& key_range,
+ concepts::string_key_value_output_iterator auto out_iter)
{
return get(txn, key_range, out_iter, commit_after_op::no_commit);
}
inline bool get(ceph::libfdb::database_handle dbh,
- const ceph::libfdb::select& key_range, auto out_iter)
+ const ceph::libfdb::select& key_range,
+ concepts::string_key_value_output_iterator auto out_iter)
{
return detail::maybe_retry(ceph::libfdb::make_transaction(dbh),
[&key_range, out_iter](transaction_handle& txn) {
co_yield std::ranges::elements_of(decoded_pairs);
}
+template <concepts::string_key_value_output_container OutContainerT>
+inline bool get(ceph::libfdb::transaction_handle txn,
+ const ceph::libfdb::select& key_range,
+ OutContainerT& out_container,
+ const ceph::libfdb::commit_after_op commit_after)
+{
+ return detail::commit_noreplay(txn, commit_after,
+ [&key_range, &out_container](const transaction_handle& txn) {
+ for (auto&& p : pair_generator(txn, key_range)) {
+ ceph::util::push_back(out_container, std::move(p));
+ }
+
+ return true;
+ });
+}
+
+template <concepts::string_key_value_output_container OutContainerT>
+inline bool get(ceph::libfdb::transaction_handle txn,
+ const ceph::libfdb::select& key_range,
+ OutContainerT& out_container)
+{
+ return get(txn, key_range, out_container, commit_after_op::no_commit);
+}
+
+template <concepts::string_key_value_output_container OutContainerT>
+inline bool get(ceph::libfdb::database_handle dbh,
+ const ceph::libfdb::select& key_range,
+ OutContainerT& out_container)
+{
+ return detail::maybe_retry(ceph::libfdb::make_transaction(dbh),
+ [&key_range, &out_container](transaction_handle& txn) {
+ return get(txn, key_range, out_container, commit_after_op::no_commit);
+ });
+}
+
// Note: block_generator() uses split planning to tackle large sets; use pair_generator() for
// direct scans.
//
add_executable(unittest_intarith test_intarith.cc)
add_ceph_unittest(unittest_intarith)
+
+add_catch2_test(concepts)
+
#make check ends here
# test_nvmeof_mon_encoding
target_link_libraries(test_nvmeof_gw_utils
mon ceph-common global-static
)
-
CHECK(std::end(out_values) != std::ranges::find(out_values, string_pair { make_key(i), make_value(i) }));
}
}
+
+ SECTION("check multiple key selection into container", "[fdb]") {
+ TestType out_values;
+
+ auto txn = lfdb::make_transaction(j);
+
+ CHECK(lfdb::get(txn,
+ lfdb::select { make_key(0), make_key(100) },
+ out_values,
+ lfdb::commit_after_op::no_commit));
+
+ CHECK(100 == out_values.size());
+ }
}
TEST_CASE("check selectors", "[fdb][rgw]") {
&std::pair<std::string, std::string>::first));
}
+ std::map<std::string, std::string> out_map;
+
+ CHECK(lfdb::get(dbh, select_all, out_map));
+ CHECK(nentries == out_map.size());
+ CHECK(make_value(0) == out_map.at(make_key(0)));
+
lfdb::set(dbh, test_key("keyx"), "outside");
out.clear();
lfdb::get(dbh, lfdb::select { make_key_prefix() }, std::back_inserter(out));
--- /dev/null
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+ */
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/catch_template_test_macros.hpp>
+
+#include "common/container_concepts.h"
+
+#include <algorithm>
+#include <array>
+#include <deque>
+#include <forward_list>
+#include <initializer_list>
+#include <list>
+#include <map>
+#include <ranges>
+#include <set>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+using ceph::concepts::associative_container;
+using ceph::concepts::can_append;
+using ceph::concepts::can_append_range;
+using ceph::concepts::can_emplace_append;
+using ceph::concepts::can_erase_if;
+using ceph::concepts::can_insert_after;
+using ceph::concepts::can_prepend;
+using ceph::concepts::can_remove_front;
+using ceph::concepts::container_appendable;
+using ceph::concepts::container_compatible_range;
+using ceph::concepts::has_size;
+using ceph::concepts::reservable_container;
+using ceph::concepts::same_as_any;
+using ceph::concepts::unordered_associative_container;
+
+// Helper gadgets for the unit tests:
+namespace {
+
+struct vector_backed_range {
+ using value_type = int;
+ using iterator = std::vector<int>::iterator;
+ using const_iterator = std::vector<int>::const_iterator;
+
+ std::vector<int> values;
+
+ vector_backed_range() = default;
+ vector_backed_range(std::initializer_list<int> init) : values(init) {}
+
+ iterator begin() { return values.begin(); }
+ iterator end() { return values.end(); }
+ const_iterator begin() const { return values.begin(); }
+ const_iterator end() const { return values.end(); }
+};
+
+// Expose iterator interface to prove can_append() and push_back() will
+// fall back to insert() when push_back()/emplace_back() don't exist:
+struct append_only final : vector_backed_range {
+ using vector_backed_range::vector_backed_range;
+
+ void insert(iterator pos, int value) {
+ values.insert(pos, value);
+ }
+};
+
+struct append_range_only final : vector_backed_range {
+ using vector_backed_range::vector_backed_range;
+
+ void append_range(const std::array<int, 3>& range) {
+ values.insert(values.end(), range.begin(), range.end());
+ }
+};
+
+struct insert_range_only final : vector_backed_range {
+ using vector_backed_range::vector_backed_range;
+
+ iterator insert_range(iterator pos, const std::array<int, 3>& range) {
+ return values.insert(pos, range.begin(), range.end());
+ }
+};
+
+// Only exposes erase(I), to help verify can_remove_front() and pop_front()
+// can work without pop_front() directly being available:
+struct front_erasable final : vector_backed_range {
+ using vector_backed_range::vector_backed_range;
+
+ iterator erase(iterator pos) {
+ return values.erase(pos);
+ }
+};
+
+// May be constructed from int, but does not convert TO one:
+struct explicit_from_int final {
+ explicit explicit_from_int(int) {}
+};
+
+template <typename RangeT, typename ValueT>
+struct range_compat_case final {
+ using range_type = RangeT;
+ using value_type = ValueT;
+};
+
+using int_predicate = bool (*)(int);
+using int_deque = std::deque<int>;
+using int_forward_list = std::forward_list<int>;
+using int_list = std::list<int>;
+using int_set = std::set<int>;
+using int_vector = std::vector<int>;
+using small_array = std::array<int, 3>;
+
+bool is_even(int value)
+{
+ return value % 2 == 0;
+}
+
+template <typename ContainerT>
+requires can_append_range<ContainerT, const std::array<int, 4>&> &&
+ can_prepend<ContainerT, int> &&
+ can_erase_if<ContainerT, int_predicate>
+ContainerT collect_odd_values()
+{
+ const std::array input{1, 2, 3, 4};
+ ContainerT values;
+
+ ceph::util::maybe_reserve(values, input.size() + 1);
+ ceph::util::append_range(values, input);
+ std::erase_if(values, is_even);
+ ceph::util::push_front(values, 0);
+
+ return values;
+}
+
+} // namespace
+
+/*** Tests for library general Concepts: */
+
+TEMPLATE_PRODUCT_TEST_CASE("standard sequence containers model input_range",
+ "[concepts]",
+ (std::vector, std::list, std::forward_list),
+ (int))
+{
+ STATIC_REQUIRE(std::ranges::input_range<TestType>);
+}
+
+TEMPLATE_TEST_CASE("contiguous sequence containers model contiguous_range",
+ "[concepts]", std::vector<int>, (std::array<int, 3>))
+{
+ STATIC_REQUIRE(std::ranges::contiguous_range<TestType>);
+}
+
+TEMPLATE_TEST_CASE("compatible ranges model container_compatible_range",
+ "[concepts]",
+ (range_compat_case<std::vector<int>, int>),
+ (range_compat_case<const std::vector<int>, int>),
+ (range_compat_case<std::array<short, 3>, int>),
+ (range_compat_case<std::string, char>),
+ (range_compat_case<std::vector<int>, const int&>))
+{
+ STATIC_REQUIRE(container_compatible_range<typename TestType::range_type,
+ typename TestType::value_type>);
+}
+
+TEMPLATE_TEST_CASE("reservable containers model reservable_container",
+ "[concepts]", std::vector<int>, std::string)
+{
+ STATIC_REQUIRE(reservable_container<TestType>);
+}
+
+TEMPLATE_TEST_CASE("appendable containers model container_appendable",
+ "[concepts]", std::vector<int>, std::deque<int>,
+ std::list<int>, std::set<int>, append_only)
+{
+ STATIC_REQUIRE(container_appendable<TestType, int>);
+}
+
+TEST_CASE("append capability avoids invalid hinted emplace paths",
+ "[concepts]")
+{
+ STATIC_REQUIRE(can_append<std::set<int>, int>);
+ STATIC_REQUIRE(container_appendable<std::set<int>, int>);
+ STATIC_REQUIRE_FALSE(ceph::concepts::has_emplace_append<std::set<int>, int>);
+ STATIC_REQUIRE(ceph::concepts::has_insert_append<std::set<int>, int>);
+}
+
+TEMPLATE_TEST_CASE("ordered associative containers model associative_container",
+ "[concepts]", std::set<int>, std::multiset<int>,
+ (std::map<int, std::string>),
+ (std::multimap<int, std::string>))
+{
+ STATIC_REQUIRE(associative_container<TestType>);
+}
+
+TEMPLATE_TEST_CASE("unordered associative containers model unordered_associative_container",
+ "[concepts]", std::unordered_set<int>,
+ std::unordered_multiset<int>,
+ (std::unordered_map<int, std::string>),
+ (std::unordered_multimap<int, std::string>))
+{
+ STATIC_REQUIRE(unordered_associative_container<TestType>);
+}
+
+/*** Tests for aggregate capability checks: */
+
+TEMPLATE_TEST_CASE("appendable containers model can_append", "[concepts]",
+ std::vector<int>, std::deque<int>, std::list<int>,
+ std::set<int>, append_only)
+{
+ STATIC_REQUIRE(can_append<TestType, int>);
+}
+
+TEMPLATE_TEST_CASE("appendable containers model can_emplace_append",
+ "[concepts]",
+ std::vector<std::string>,
+ std::deque<std::string>,
+ std::list<std::string>,
+ std::set<std::string>)
+{
+ STATIC_REQUIRE(can_emplace_append<TestType, std::string_view>);
+}
+
+TEMPLATE_PRODUCT_TEST_CASE("prependable containers model can_prepend",
+ "[concepts]",
+ (std::deque, std::list, std::forward_list,
+ std::vector),
+ (int))
+{
+ STATIC_REQUIRE(can_prepend<TestType, int>);
+}
+
+TEMPLATE_PRODUCT_TEST_CASE("standard range appendable containers model "
+ "can_append_range",
+ "[concepts]", (std::vector, std::set), (int))
+{
+ STATIC_REQUIRE(can_append_range<TestType, std::array<int, 3>>);
+}
+
+TEST_CASE("forward-only containers model can_insert_after",
+ "[concepts]")
+{
+ STATIC_REQUIRE(can_insert_after<int_forward_list,
+ int_forward_list::iterator,
+ int>);
+ STATIC_REQUIRE_FALSE(can_insert_after<int_vector,
+ int_vector::iterator,
+ int>);
+}
+
+TEMPLATE_PRODUCT_TEST_CASE("predicate erasable containers model can_erase_if",
+ "[concepts]",
+ (std::vector, std::list, std::forward_list),
+ (int))
+{
+ STATIC_REQUIRE(can_erase_if<TestType, int_predicate>);
+}
+
+TEMPLATE_PRODUCT_TEST_CASE("standard front removable containers model "
+ "can_remove_front",
+ "[concepts]",
+ (std::deque, std::list, std::forward_list,
+ std::vector),
+ (int))
+{
+ STATIC_REQUIRE(can_remove_front<TestType>);
+}
+
+TEST_CASE("concept edge cases",
+ "[concepts]")
+{
+ STATIC_REQUIRE(same_as_any<int, double, int, std::string>);
+ STATIC_REQUIRE_FALSE(same_as_any<int, double, long, std::string>);
+ STATIC_REQUIRE_FALSE(std::ranges::contiguous_range<std::deque<int>>);
+ STATIC_REQUIRE_FALSE(container_compatible_range<std::vector<int>,
+ std::string>);
+ STATIC_REQUIRE_FALSE(container_compatible_range<std::vector<int>,
+ explicit_from_int>);
+ STATIC_REQUIRE_FALSE(container_compatible_range<int, int>);
+ STATIC_REQUIRE_FALSE(associative_container<std::vector<int>>);
+ STATIC_REQUIRE_FALSE(unordered_associative_container<std::set<int>>);
+ STATIC_REQUIRE_FALSE(can_append<std::forward_list<int>, int>);
+ STATIC_REQUIRE_FALSE(can_emplace_append<std::forward_list<int>, int>);
+ STATIC_REQUIRE_FALSE(container_appendable<std::forward_list<int>, int>);
+ STATIC_REQUIRE(can_append_range<append_only, std::array<int, 3>>);
+ STATIC_REQUIRE_FALSE(can_append_range<std::forward_list<int>,
+ std::array<int, 3>>);
+ STATIC_REQUIRE_FALSE(has_size<std::forward_list<int>>);
+ STATIC_REQUIRE_FALSE(reservable_container<std::list<int>>);
+ STATIC_REQUIRE(can_remove_front<front_erasable>);
+}
+
+/*** Tests for library helpers: */
+
+TEMPLATE_PRODUCT_TEST_CASE("helpers compose into container-generic algorithms",
+ "[concepts][util]",
+ (std::deque, std::list, std::vector),
+ (int))
+{
+ const TestType expected{0, 1, 3};
+
+ CHECK(collect_odd_values<TestType>() == expected);
+}
+
+TEST_CASE("push_back appends through the best available container operation",
+ "[concepts][util]")
+{
+ SECTION("vector uses back insertion") {
+ std::vector<int> values;
+
+ ceph::util::push_back(values, 1);
+ ceph::util::push_back(values, 2);
+
+ CHECK(values == std::vector{1, 2});
+ }
+
+ SECTION("set falls back to hinted insertion at end") {
+ std::set<int> values;
+
+ ceph::util::push_back(values, 2);
+ ceph::util::push_back(values, 1);
+
+ CHECK(values == std::set{1, 2});
+ }
+
+ SECTION("custom append-only type uses insert(end, value)") {
+ append_only values;
+
+ ceph::util::push_back(values, 1);
+ ceph::util::push_back(values, 2);
+
+ CHECK(values.values == std::vector{1, 2});
+ }
+}
+
+TEST_CASE("emplace_append constructs at the natural append position",
+ "[concepts][util]")
+{
+ SECTION("vector constructs from string_view") {
+ std::vector<std::string> values;
+
+ ceph::util::emplace_append(values, std::string_view { "alpha" });
+ ceph::util::emplace_append(values, std::string_view { "beta" });
+
+ CHECK(values == std::vector<std::string> { "alpha", "beta" });
+ }
+
+ SECTION("list constructs from string_view") {
+ std::list<std::string> values;
+
+ ceph::util::emplace_append(values, std::string_view { "alpha" });
+ ceph::util::emplace_append(values, std::string_view { "beta" });
+
+ CHECK(values == std::list<std::string> { "alpha", "beta" });
+ }
+
+ SECTION("set falls back to value insertion") {
+ std::set<std::string> values;
+
+ ceph::util::emplace_append(values, std::string_view { "beta" });
+ ceph::util::emplace_append(values, std::string_view { "alpha" });
+
+ CHECK(values == std::set<std::string> { "alpha", "beta" });
+ }
+}
+
+TEST_CASE("append_range appends ranges with container-specific fallbacks",
+ "[concepts][util]")
+{
+ SECTION("vector appends iterator ranges") {
+ std::vector<int> values{1};
+ const std::array more{2, 3};
+
+ ceph::util::append_range(values, more);
+
+ CHECK(values == std::vector{1, 2, 3});
+ }
+
+ SECTION("set inserts an input range") {
+ std::set<int> values{1};
+ const std::array more{3, 2};
+
+ ceph::util::append_range(values, more);
+
+ CHECK(values == std::set{1, 2, 3});
+ }
+
+ SECTION("custom append-only type falls back to element appends") {
+ append_only values;
+ const std::array more{1, 2, 3};
+
+ ceph::util::append_range(values, more);
+
+ CHECK(values.values == std::vector{1, 2, 3});
+ }
+}
+
+TEST_CASE("collect_as materializes ranges with append fallbacks",
+ "[concepts][util]")
+{
+ SECTION("sequence from view") {
+ auto values = ceph::util::collect_as<std::vector<int>>(
+ std::views::iota(0, 4));
+
+ CHECK(values == std::vector{0, 1, 2, 3});
+ }
+
+ SECTION("set from transformed view") {
+ auto values = ceph::util::collect_as<std::set<int>>(
+ std::views::iota(0, 4) |
+ std::views::transform([](int i) { return 3 - i; }));
+
+ CHECK(values == std::set{0, 1, 2, 3});
+ }
+
+ SECTION("custom append-only type") {
+ auto values = ceph::util::collect_as<append_only>(
+ std::array{1, 2, 3});
+
+ CHECK(values.values == std::vector{1, 2, 3});
+ }
+}
+
+TEST_CASE("insert_range inserts at the requested position", "[concepts][util]")
+{
+ std::vector<int> values{1, 4};
+ const std::array more{2, 3};
+
+ ceph::util::insert_range(values, values.begin() + 1, more);
+
+ CHECK(values == std::vector{1, 2, 3, 4});
+}
+
+TEST_CASE("insert_after inserts after the requested position",
+ "[concepts][util]")
+{
+ std::forward_list<int> values;
+ auto pos = values.before_begin();
+
+ pos = ceph::util::insert_after(values, pos, 1);
+ pos = ceph::util::emplace_after(values, pos, 2);
+ ceph::util::insert_after(values, pos, 3);
+
+ CHECK(values == std::forward_list{1, 2, 3});
+}
+
+TEST_CASE("make_appender appends through container-specific operations",
+ "[concepts][util]")
+{
+ SECTION("vector appender constructs at the back") {
+ std::vector<std::string> values;
+ auto append = ceph::util::make_appender(values);
+
+ append.emplace(std::string_view { "alpha" });
+ append.emplace(std::string_view { "beta" });
+
+ CHECK(values == std::vector<std::string> { "alpha", "beta" });
+ }
+
+ SECTION("forward_list appender tracks the insertion position") {
+ std::forward_list<std::string> values;
+ auto append = ceph::util::make_appender(values);
+
+ append.emplace(std::string_view { "alpha" });
+ append.emplace(std::string_view { "beta" });
+
+ CHECK(values == std::forward_list<std::string> { "alpha", "beta" });
+ }
+}
+
+TEST_CASE("front helpers use front-specific operations or begin erasure",
+ "[concepts][util]")
+{
+ SECTION("push_front prepends values") {
+ std::vector<int> values{2, 3};
+
+ ceph::util::push_front(values, 1);
+
+ CHECK(values == std::vector{1, 2, 3});
+ }
+
+ SECTION("pop_front removes the first value") {
+ front_erasable values{{1, 2, 3}};
+
+ ceph::util::pop_front(values);
+
+ CHECK(values.values == std::vector{2, 3});
+ }
+}
+
+TEST_CASE("optional sizing helpers call supported operations only",
+ "[concepts][util]")
+{
+ std::vector<int> values;
+
+ ceph::util::maybe_reserve(values, 4);
+ ceph::util::maybe_resize(values, 3);
+
+ CHECK(ceph::util::capacity(values) >= 4);
+ CHECK(std::ranges::size(values) == 3);
+ CHECK(!std::ranges::empty(values));
+
+ ceph::util::clear(values);
+
+ CHECK(std::ranges::empty(values));
+}
+
+/* The use of X-macros here is a bit unfortunate, however as we're not yet using C++26 I
+ * don't think there's another way to express this without having... well, a *LOT* of essentially
+ * redundant tests: */
+#define CEPH_HAS_MEMBER_CAPABILITY_CASES(X) \
+ X(emplace_back, (ceph::concepts::has_emplace_back<int_vector, int>), \
+ (ceph::concepts::has_emplace_back<int_set, int>)) \
+ X(push_back, (ceph::concepts::has_push_back<int_vector, int>), \
+ (ceph::concepts::has_push_back<int_set, int>)) \
+ X(emplace_append, (ceph::concepts::has_emplace_append<int_vector, int>), \
+ (ceph::concepts::has_emplace_append<int_set, int>)) \
+ X(insert_append, (ceph::concepts::has_insert_append<int_vector, int>), \
+ (ceph::concepts::has_insert_append<int_forward_list, int>)) \
+ X(emplace_after, (ceph::concepts::has_emplace_after<int_forward_list, int_forward_list::iterator, int>), \
+ (ceph::concepts::has_emplace_after<int_vector, int_vector::iterator, int>)) \
+ X(insert_after, (ceph::concepts::has_insert_after<int_forward_list, int_forward_list::iterator, int>), \
+ (ceph::concepts::has_insert_after<int_vector, int_vector::iterator, int>)) \
+ X(append_range, (ceph::concepts::has_append_range<append_range_only, small_array>), \
+ (ceph::concepts::has_append_range<int_vector, small_array>)) \
+ X(insert_range_append, (ceph::concepts::has_insert_range_append<insert_range_only, small_array>), \
+ (ceph::concepts::has_insert_range_append<int_vector, small_array>)) \
+ X(insert_iterator_range_append, (ceph::concepts::has_insert_iterator_range_append<int_vector, small_array>), \
+ (ceph::concepts::has_insert_iterator_range_append<int_forward_list, small_array>)) \
+ X(insert_range, (ceph::concepts::has_insert_range<insert_range_only, insert_range_only::iterator, small_array>), \
+ (ceph::concepts::has_insert_range<int_vector, int_vector::iterator, small_array>)) \
+ X(insert_iterator_range, (ceph::concepts::has_insert_iterator_range<int_vector, int_vector::iterator, small_array>), \
+ (ceph::concepts::has_insert_iterator_range<int_forward_list, int_forward_list::iterator, small_array>)) \
+ X(emplace_front, (ceph::concepts::has_emplace_front<int_deque, int>), \
+ (ceph::concepts::has_emplace_front<int_vector, int>)) \
+ X(push_front, (ceph::concepts::has_push_front<int_deque, int>), \
+ (ceph::concepts::has_push_front<int_vector, int>)) \
+ X(emplace_at_begin, (ceph::concepts::has_emplace_at_begin<int_vector, int>), \
+ (ceph::concepts::has_emplace_at_begin<int_forward_list, int>)) \
+ X(insert_at_begin, (ceph::concepts::has_insert_at_begin<int_vector, int>), \
+ (ceph::concepts::has_insert_at_begin<int_forward_list, int>)) \
+ X(pop_front, (ceph::concepts::has_pop_front<int_deque>), \
+ (ceph::concepts::has_pop_front<int_vector>)) \
+ X(erase_begin, (ceph::concepts::has_erase_begin<int_vector>), \
+ (ceph::concepts::has_erase_begin<int_forward_list>)) \
+ X(erase, (ceph::concepts::has_erase<int_set, int>), \
+ (ceph::concepts::has_erase<int_vector, int>)) \
+ X(erase_range, (ceph::concepts::has_erase_range<int_vector, int_vector::iterator, int_vector::iterator>), \
+ (ceph::concepts::has_erase_range<int_forward_list, int_forward_list::iterator, int_forward_list::iterator>)) \
+ X(remove_if, (ceph::concepts::has_remove_if<int_list, int_predicate>), \
+ (ceph::concepts::has_remove_if<int_vector, int_predicate>)) \
+ X(clear, (ceph::concepts::has_clear<int_vector>), \
+ (ceph::concepts::has_clear<int>)) \
+ X(reserve, (ceph::concepts::has_reserve<int_vector>), \
+ (ceph::concepts::has_reserve<int_list>)) \
+ X(capacity, (ceph::concepts::has_capacity<int_vector>), \
+ (ceph::concepts::has_capacity<int_list>)) \
+ X(max_size, (ceph::concepts::has_max_size<int_vector>), \
+ (ceph::concepts::has_max_size<int>)) \
+ X(size, (ceph::concepts::has_size<int_vector>), \
+ (ceph::concepts::has_size<int_forward_list>)) \
+ X(empty, (ceph::concepts::has_empty<int_vector>), \
+ (ceph::concepts::has_empty<int>)) \
+ X(resize, (ceph::concepts::has_resize<int_vector>), \
+ (ceph::concepts::has_resize<int_set>))
+
+#define CEPH_CHECK_HAS_MEMBER_CAPABILITY(member, positive, negative) \
+ STATIC_REQUIRE positive; \
+ STATIC_REQUIRE_FALSE negative;
+
+TEST_CASE("has_* member capability predicates match member availability",
+ "[concepts]")
+{
+ CEPH_HAS_MEMBER_CAPABILITY_CASES(CEPH_CHECK_HAS_MEMBER_CAPABILITY)
+}
+
+// Put the X-macro machinery back into the original packaging:
+#undef CEPH_CHECK_HAS_MEMBER_CAPABILITY
+#undef CEPH_HAS_MEMBER_CAPABILITY_CASES