]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
Improve Concept vocabulary for libfdb
authorJesse F. Williamson <jfw@ibm.com>
Fri, 12 Jun 2026 16:28:11 +0000 (09:28 -0700)
committerJesse F. Williamson <jfw@ibm.com>
Fri, 31 Jul 2026 19:55:15 +0000 (12:55 -0700)
Extend a stronger vocabulary of C++ Concepts and helpful functions
via an internal collection of some Concepts not currently in the
C++ Standard along with useful predicates for compile-time capability
queries and runtime feature invocation.

The mini-library provides general capabilities meant for writing clean
algorithms, doing metaprogramming, or offering better support for
general operations involving sequences, and so on:

Concepts for common container categories (like contiguous sequence,
ordered associative, etc.);

Capability queries for container operations such as append, prepend,
range insertion, etc.;

Aggregate capability checks such as “can_append”, “can_append_range”,
“can_erase_if”, etc.;

Utility helpers that select appropriate operations for containers, like “push_back”,
"insert_front", etc.;

The tests cover standard STL containers, custom minimal test containers for
fallback behavior, negative concept edge cases, and an example container-generic
algorithm that composes several helpers.

Assisted-by: Codex:GPT-5
Signed-off-by: Jesse F. Williamson <jfw@ibm.com>
src/common/container_concepts.h [new file with mode: 0644]
src/rgw/fdb/EXAMPLES.md
src/rgw/fdb/base.h
src/rgw/fdb/interface.h
src/test/CMakeLists.txt
src/test/rgw/test_fdb.cc
src/test/test_concepts.cc [new file with mode: 0644]

diff --git a/src/common/container_concepts.h b/src/common/container_concepts.h
new file mode 100644 (file)
index 0000000..d873662
--- /dev/null
@@ -0,0 +1,665 @@
+// -*- 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
index e23422f1eea46f4d2fb5df4870c11b90048386c8..4ef5fd6f5b7ee66066216dc878d100daeb1aa424 100644 (file)
@@ -136,9 +136,7 @@ lfdb::set(dbh, std::begin(people), std::end(people));
 /* 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
index c67804318896a12722e9a274986993a1344ad1e9..3341e3515591bea634d12bb8dc801796c688a2c6 100644 (file)
@@ -45,6 +45,8 @@
 #include <filesystem>
 #include <type_traits>
 
+#include "common/container_concepts.h"
+
 #ifdef __cpp_lib_flat_map
  #include <flat_map>
  template <typename ...Args>
@@ -70,38 +72,6 @@ extern transaction_handle make_transaction(database_handle dbh);
 
 } // 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
@@ -117,6 +87,14 @@ concept key_value_iterator =
   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>>;
@@ -137,10 +115,39 @@ concept supported_invocation_result =
 // 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 {
 
@@ -576,7 +583,7 @@ class transaction final
  // 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);
  }
@@ -623,7 +630,10 @@ class transaction final
                         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);
@@ -868,9 +878,7 @@ inline auto decode_pairs(std::span<const FDBKeyValue> pairs)
 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>
@@ -892,7 +900,7 @@ inline query_window_result<AssocT> materialize_query_window(transaction& txn, se
 }
 
 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;
@@ -930,23 +938,23 @@ inline std::vector<ceph::libfdb::select> as_select_seq(std::span<const FDBKey> x
  }
 
  // 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
index 7ad4183b2b60bf154e5738707d90f2c9554315a3..60c96560928d84b0c4c59f3dfb0a3427ad6de994 100644 (file)
@@ -272,7 +272,8 @@ namespace ceph::libfdb {
 // 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,
@@ -282,13 +283,15 @@ inline bool get(ceph::libfdb::transaction_handle txn,
 }
 
 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) {
@@ -434,6 +437,41 @@ inline auto pair_generator(ceph::libfdb::transaction_handle txn, ceph::libfdb::s
  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.
 //
index 2df929008e6aea3a7f646fdca2b022ce1d2fc3d7..8efc08d459b7bf3913d2fab2131a397f53d58844 100644 (file)
@@ -1060,6 +1060,9 @@ add_ceph_unittest(unittest_weighted_shuffle)
 
 add_executable(unittest_intarith test_intarith.cc)
 add_ceph_unittest(unittest_intarith)
+
+add_catch2_test(concepts)
+
 #make check ends here
 
 # test_nvmeof_mon_encoding
@@ -1109,4 +1112,3 @@ add_executable(test_nvmeof_gw_utils
 target_link_libraries(test_nvmeof_gw_utils
   mon ceph-common global-static
   )
-
index 66b1bd551ccd4ca220d7238fd70073c820c062b9..6c71552a48af79bd86d6466152b5c33d4a706962 100644 (file)
@@ -269,6 +269,19 @@ TEMPLATE_PRODUCT_TEST_CASE("multi-key ops", "[rgw][fdb]",
     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]") {
@@ -339,6 +352,12 @@ 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));
diff --git a/src/test/test_concepts.cc b/src/test/test_concepts.cc
new file mode 100644 (file)
index 0000000..20ae19f
--- /dev/null
@@ -0,0 +1,587 @@
+/*
+ * 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