Let `HybridDirectMap` API look more like a typical associative container.

Add `iterator` and `const_iterator`.

Rename `Find()` returning a pointer to `FindOrNull()`. Add `find()` returning an
iterator. The performance of `find()` and `FindOrNull()` is comparable, but if
the semantics of `FindOrNull()` is needed (such as in `FieldHandlerMap`), then
the dedicated function is more readable and can be faster.

Add `FindOrDefault()`, `contains()`, `at()`, `empty()`, `size()`, `{,c}begin()`,
`{,c}end()`, and `operator==`.

PiperOrigin-RevId: 889778092
diff --git a/riegeli/base/BUILD b/riegeli/base/BUILD
index a65830d..fb5861f 100644
--- a/riegeli/base/BUILD
+++ b/riegeli/base/BUILD
@@ -663,6 +663,8 @@
     deps = [
         ":arithmetic",
         ":assert",
+        ":compare",
+        ":debug",
         ":iterable",
         ":type_traits",
         "@com_google_absl//absl/base:core_headers",
diff --git a/riegeli/base/hybrid_direct_common.h b/riegeli/base/hybrid_direct_common.h
index 16640ff..0fb05eb 100644
--- a/riegeli/base/hybrid_direct_common.h
+++ b/riegeli/base/hybrid_direct_common.h
@@ -44,6 +44,11 @@
     return static_cast<RawKey>(static_cast<RawKey>(key) -
                                static_cast<RawKey>(expected_min_key));
   }
+
+  static Key FromRawKey(RawKey raw_key) {
+    // Wrap-around is not an error.
+    return static_cast<Key>(raw_key + static_cast<RawKey>(expected_min_key));
+  }
 };
 
 template <typename Key, Key expected_min_key>
@@ -56,6 +61,11 @@
     return static_cast<RawKey>(static_cast<RawKey>(key) -
                                static_cast<RawKey>(expected_min_key));
   }
+
+  static Key FromRawKey(RawKey raw_key) {
+    // Wrap-around is not an error.
+    return static_cast<Key>(raw_key + static_cast<RawKey>(expected_min_key));
+  }
 };
 
 // The default `direct_capacity` parameter for `HybridDirectMap` building.
diff --git a/riegeli/base/hybrid_direct_map.h b/riegeli/base/hybrid_direct_map.h
index 7c5fa79..71aaa4a 100644
--- a/riegeli/base/hybrid_direct_map.h
+++ b/riegeli/base/hybrid_direct_map.h
@@ -21,6 +21,7 @@
 #include <initializer_list>
 #include <iterator>
 #include <memory>
+#include <optional>
 #include <type_traits>
 #include <utility>
 
@@ -30,6 +31,8 @@
 #include "absl/container/flat_hash_map.h"
 #include "riegeli/base/arithmetic.h"
 #include "riegeli/base/assert.h"
+#include "riegeli/base/compare.h"
+#include "riegeli/base/debug.h"
 #include "riegeli/base/hybrid_direct_common.h"  // IWYU pragma: export
 #include "riegeli/base/hybrid_direct_internal.h"
 #include "riegeli/base/iterable.h"
@@ -46,13 +49,63 @@
 // conditionally.
 template <typename Key, typename Value, typename Traits>
 class HybridDirectMapImpl {
+ private:
+  template <bool is_const>
+  class IteratorImpl;
+
  public:
+  using key_type = Key;
+  using mapped_type = Value;
+  using value_type = std::pair<const Key, Value>;
+  using reference = ReferencePair<const Key, Value&>;
+  using const_reference = ReferencePair<const Key, const Value&>;
+  using pointer = ArrowProxy<reference>;
+  using const_pointer = ArrowProxy<const_reference>;
+  using iterator = IteratorImpl<false>;
+  using const_iterator = IteratorImpl<true>;
+  using size_type = size_t;
+  using difference_type = ptrdiff_t;
+
   static size_t max_size();
 
   ABSL_ATTRIBUTE_REINITIALIZES void Reset();
 
-  Value* absl_nullable Find(Key key);
-  const Value* absl_nullable Find(Key key) const;
+  // Returns a pointer to the value associated with `key`, or `nullptr` if `key`
+  // is absent.
+  //
+  // This can be a bit faster than `find()`.
+  Value* absl_nullable FindOrNull(Key key) ABSL_ATTRIBUTE_LIFETIME_BOUND;
+  const Value* absl_nullable FindOrNull(Key key) const
+      ABSL_ATTRIBUTE_LIFETIME_BOUND;
+
+  // Returns a reference to the value associated with `key`, or a reference to
+  // `default_value` if `key` is absent.
+  const Value& FindOrDefault(
+      Key key, const Value& default_value ABSL_ATTRIBUTE_LIFETIME_BOUND = {})
+      const ABSL_ATTRIBUTE_LIFETIME_BOUND;
+
+  iterator find(Key key) ABSL_ATTRIBUTE_LIFETIME_BOUND;
+  const_iterator find(Key key) const ABSL_ATTRIBUTE_LIFETIME_BOUND;
+
+  bool contains(Key key) const;
+
+  Value& at(Key key) ABSL_ATTRIBUTE_LIFETIME_BOUND;
+  const Value& at(Key key) const ABSL_ATTRIBUTE_LIFETIME_BOUND;
+
+  bool empty() const {
+    return direct_values_.get_deleter().size() == 0 &&
+           ABSL_PREDICT_TRUE(slow_map_ == nullptr);
+  }
+  size_t size() const;
+
+  iterator begin() ABSL_ATTRIBUTE_LIFETIME_BOUND;
+  const_iterator begin() const ABSL_ATTRIBUTE_LIFETIME_BOUND;
+  const_iterator cbegin() const ABSL_ATTRIBUTE_LIFETIME_BOUND {
+    return begin();
+  }
+  iterator end() ABSL_ATTRIBUTE_LIFETIME_BOUND;
+  const_iterator end() const ABSL_ATTRIBUTE_LIFETIME_BOUND;
+  const_iterator cend() const ABSL_ATTRIBUTE_LIFETIME_BOUND { return end(); }
 
  protected:
   HybridDirectMapImpl() = default;
@@ -68,6 +121,8 @@
                   const ValueProjection& value_projection,
                   size_t direct_capacity);
 
+  static bool Equal(const HybridDirectMapImpl& a, const HybridDirectMapImpl& b);
+
  private:
   using RawKey = std::decay_t<decltype(Traits::ToRawKey(std::declval<Key>()))>;
   static_assert(std::is_unsigned_v<RawKey>);
@@ -90,6 +145,15 @@
   CopyDirectMap(DelayedConstructor<Value>* absl_nullable dest_values) const;
   absl_nullable std::unique_ptr<SlowMap> CopySlowMap() const;
 
+  ABSL_ATTRIBUTE_NORETURN static void KeyNotFound(Key key);
+
+  size_t FirstRawKey() const;
+
+  size_t capacity() const {
+    return direct_map_.get_deleter().size() +
+           (slow_map_ == nullptr ? 0 : slow_map_->capacity());
+  }
+
   // Stores values for `direct_map_`, in no particular order.
   absl_nullable DirectValues direct_values_;
   // Indexed by raw key below `direct_map_.get_deleter().size()`. Elements
@@ -106,19 +170,25 @@
 
 }  // namespace hybrid_direct_internal
 
-// `HybridDirectMap` is a map optimized for keys being small integers.
-// It supports only lookups, but no incremental building nor iteration.
+// `HybridDirectMap` is a map optimized for keys being mostly small integers,
+// especially dense near zero. It supports only lookups and iteration, but no
+// incremental modification.
 //
 // It stores a part of the map covering some range of small keys in an array.
 // The remaining keys are stored in an `absl::flat_hash_map`.
 //
 // `Traits` specifies a mapping of keys to an unsigned integer type. It must
-// support at least the following static member:
+// support at least the following static members:
 //
 // ```
 //   // Translates the key to a raw key, which is an unsigned integer type.
 //   // Small raw keys are put in the array.
 //   static RawKey ToRawKey(Key key);
+//
+//   // Translates the raw key back to a key.
+//   //
+//   // This is optional. Needed only for iterators.
+//   static Key FromRawKey(RawKey raw_key);
 // ```
 //
 // `direct_capacity`, if specified during building, is the intended capacity
@@ -132,7 +202,8 @@
       private ConditionallyConstructible<std::is_copy_constructible_v<Value>,
                                          true>,
       private ConditionallyAssignable<std::is_copy_constructible_v<Value>,
-                                      true> {
+                                      true>,
+      public WithEqual<HybridDirectMap<Key, Value, Traits>> {
  private:
   template <typename Src, typename Enable = void>
   struct HasCompatibleKeys : std::false_type {};
@@ -313,8 +384,147 @@
     this->Initialize(std::forward<Src>(src), key_projection, value_projection,
                      direct_capacity);
   }
+
+  friend bool operator==(const HybridDirectMap& a, const HybridDirectMap& b) {
+    return HybridDirectMap::HybridDirectMapImpl::Equal(a, b);
+  }
 };
 
+namespace hybrid_direct_internal {
+
+template <typename Key, typename Value, typename Traits>
+template <bool is_const>
+class HybridDirectMapImpl<Key, Value, Traits>::IteratorImpl
+    : public WithEqual<IteratorImpl<is_const>> {
+ public:
+  using iterator_concept = std::forward_iterator_tag;
+  // `iterator_category` is only `std::input_iterator_tag` because the
+  // `LegacyForwardIterator` requirement and above require `reference` to be
+  // a true reference type.
+  using iterator_category = std::input_iterator_tag;
+  using value_type = std::pair<const Key, Value>;
+  using reference =
+      ReferencePair<const Key,
+                    std::conditional_t<is_const, const Value&, Value&>>;
+  using pointer = ArrowProxy<reference>;
+  using difference_type = ptrdiff_t;
+
+  IteratorImpl() = default;
+
+  // Conversion from `iterator` to `const_iterator`.
+  template <bool that_is_const,
+            std::enable_if_t<is_const && !that_is_const, int> = 0>
+  /*implicit*/ IteratorImpl(IteratorImpl<that_is_const> that) noexcept
+      : direct_map_end_(that.direct_map_end_),
+        direct_map_size_(that.direct_map_size_),
+        raw_key_complement_(that.raw_key_complement_),
+        slow_map_iter_(that.slow_map_iter_) {}
+
+  IteratorImpl(const IteratorImpl& that) = default;
+  IteratorImpl& operator=(const IteratorImpl& that) = default;
+
+  reference operator*() const {
+    if (ABSL_PREDICT_TRUE(raw_key_complement_ > 0)) {
+      return reference{Traits::FromRawKey(IntCast<RawKey>(direct_map_size_ -
+                                                          raw_key_complement_)),
+                       **(direct_map_end_ - raw_key_complement_)};
+    }
+    const auto iter = *slow_map_iter_;
+    return reference{Traits::FromRawKey(iter->first), iter->second};
+  }
+  pointer operator->() const { return pointer(**this); }
+  IteratorImpl& operator++() {
+    if (ABSL_PREDICT_TRUE(raw_key_complement_ > 0)) {
+      do {
+        --raw_key_complement_;
+        if (ABSL_PREDICT_FALSE(raw_key_complement_ == 0)) break;
+      } while (*(direct_map_end_ - raw_key_complement_) == nullptr);
+    } else {
+      ++*slow_map_iter_;
+    }
+    return *this;
+  }
+  IteratorImpl operator++(int) {
+    IteratorImpl result = *this;
+    ++*this;
+    return result;
+  }
+
+  template <bool that_is_const>
+  friend bool operator==(IteratorImpl a, IteratorImpl<that_is_const> b) {
+    RIEGELI_ASSERT_EQ(a.direct_map_end_, b.direct_map_end_)
+        << "Failed precondition of operator==(HybridDirectMap::iterator): "
+           "incomparable iterators";
+    RIEGELI_ASSERT_EQ(a.direct_map_size_, b.direct_map_size_)
+        << "Failed precondition of operator==(HybridDirectMap::iterator): "
+           "incomparable iterators";
+    RIEGELI_ASSERT_EQ(a.slow_map_iter_ != std::nullopt,
+                      b.slow_map_iter_ != std::nullopt)
+        << "Failed precondition of operator==(HybridDirectMap::iterator): "
+           "incomparable iterators";
+    if (a.raw_key_complement_ != b.raw_key_complement_) return false;
+    if (ABSL_PREDICT_TRUE(a.slow_map_iter_ == std::nullopt)) return true;
+    return *a.slow_map_iter_ == *b.slow_map_iter_;
+  }
+
+ private:
+  friend class HybridDirectMapImpl;
+
+  explicit IteratorImpl(std::conditional_t<is_const, const HybridDirectMapImpl*,
+                                           HybridDirectMapImpl*>
+                            map ABSL_ATTRIBUTE_LIFETIME_BOUND,
+                        size_t raw_key_complement)
+      : direct_map_end_(map->direct_map_.get() +
+                        map->direct_map_.get_deleter().size()),
+        direct_map_size_(map->direct_map_.get_deleter().size()),
+        raw_key_complement_(raw_key_complement) {}
+
+  explicit IteratorImpl(
+      std::conditional_t<is_const, const HybridDirectMapImpl*,
+                         HybridDirectMapImpl*>
+          map ABSL_ATTRIBUTE_LIFETIME_BOUND,
+      size_t raw_key_complement,
+      std::conditional_t<is_const, typename SlowMap::const_iterator,
+                         typename SlowMap::iterator>
+          slow_map_iter)
+      : direct_map_end_(map->direct_map_.get() +
+                        map->direct_map_.get_deleter().size()),
+        direct_map_size_(map->direct_map_.get_deleter().size()),
+        raw_key_complement_(raw_key_complement),
+        slow_map_iter_(slow_map_iter) {}
+
+  // The end of the `direct_map_` array.
+  //
+  // Counting backwards simplifies checking for iteration over `direct_map_`.
+  absl_nullable const std::conditional_t<
+      is_const, const Value*, Value*>* absl_nullable direct_map_end_ = nullptr;
+  // `direct_map_.get_deleter().size()`.
+  size_t direct_map_size_ = 0;
+  // `direct_map_size_ - raw_key` when iterating over `direct_map_`, otherwise
+  // 0.
+  //
+  // Invariant: if `raw_key_complement > 0` then
+  // `*(direct_map_end_ - raw_key_complement_) != nullptr`.
+  //
+  // Counting backwards simplifies checking for iteration over `direct_map_`.
+  size_t raw_key_complement_ = 0;
+  // Iterator over `*slow_map_` when `slow_map_ != nullptr`, otherwise
+  // `std::nullopt`.
+  //
+  // Invariant: if `raw_key_complement_ > 0` then
+  // `slow_map_iter_ == std::nullopt` or
+  // `slow_map_iter_ == slow_map_->begin()`.
+  //
+  // Distinguishing `std::nullopt` instead of using the default-constructed
+  // `SlowMap::iterator` makes the common case of `operator==` faster by
+  // reducing usage of `SlowMap` iterators.
+  std::optional<std::conditional_t<is_const, typename SlowMap::const_iterator,
+                                   typename SlowMap::iterator>>
+      slow_map_iter_;
+};
+
+}  // namespace hybrid_direct_internal
+
 // Implementation details follow.
 
 namespace hybrid_direct_internal {
@@ -529,14 +739,16 @@
 }
 
 template <typename Key, typename Value, typename Traits>
-ABSL_ATTRIBUTE_ALWAYS_INLINE Value* absl_nullable
-HybridDirectMapImpl<Key, Value, Traits>::Find(Key key) {
-  return const_cast<Value*>(std::as_const(*this).Find(key));
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline Value* absl_nullable
+HybridDirectMapImpl<Key, Value, Traits>::FindOrNull(Key key)
+    ABSL_ATTRIBUTE_LIFETIME_BOUND {
+  return const_cast<Value*>(std::as_const(*this).FindOrNull(key));
 }
 
 template <typename Key, typename Value, typename Traits>
-ABSL_ATTRIBUTE_ALWAYS_INLINE const Value* absl_nullable
-HybridDirectMapImpl<Key, Value, Traits>::Find(Key key) const {
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline const Value* absl_nullable
+HybridDirectMapImpl<Key, Value, Traits>::FindOrNull(Key key) const
+    ABSL_ATTRIBUTE_LIFETIME_BOUND {
   RIEGELI_ASSERT(!direct_map_.get_deleter().IsMovedFromIfNull() ||
                  direct_map_ != nullptr)
       << "Moved-from HybridDirectMap";
@@ -548,6 +760,193 @@
   return &iter->second;
 }
 
+template <typename Key, typename Value, typename Traits>
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline const Value&
+HybridDirectMapImpl<Key, Value, Traits>::FindOrDefault(
+    Key key, const Value& default_value ABSL_ATTRIBUTE_LIFETIME_BOUND) const
+    ABSL_ATTRIBUTE_LIFETIME_BOUND {
+  RIEGELI_ASSERT(!direct_map_.get_deleter().IsMovedFromIfNull() ||
+                 direct_map_ != nullptr)
+      << "Moved-from HybridDirectMap";
+  const RawKey raw_key = Traits::ToRawKey(key);
+  if (raw_key < direct_map_.get_deleter().size()) {
+    const Value* const absl_nullable value = direct_map_[raw_key];
+    if (value == nullptr) return default_value;
+    return *value;
+  }
+  if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) return default_value;
+  const auto iter = slow_map_->find(raw_key);
+  if (iter == slow_map_->end()) return default_value;
+  return iter->second;
+}
+
+template <typename Key, typename Value, typename Traits>
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline auto
+HybridDirectMapImpl<Key, Value, Traits>::find(Key key)
+    ABSL_ATTRIBUTE_LIFETIME_BOUND -> iterator {
+  RIEGELI_ASSERT(!direct_map_.get_deleter().IsMovedFromIfNull() ||
+                 direct_map_ != nullptr)
+      << "Moved-from HybridDirectMap";
+  const RawKey raw_key = Traits::ToRawKey(key);
+  if (raw_key < direct_map_.get_deleter().size()) {
+    if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) {
+      return iterator(this, direct_map_[raw_key] == nullptr
+                                ? 0
+                                : direct_map_.get_deleter().size() - raw_key);
+    }
+    if (direct_map_[raw_key] == nullptr) {
+      return iterator(this, 0, slow_map_->end());
+    }
+    return iterator(this, direct_map_.get_deleter().size() - raw_key,
+                    slow_map_->begin());
+  }
+  if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) return iterator(this, 0);
+  return iterator(this, 0, slow_map_->find(raw_key));
+}
+
+template <typename Key, typename Value, typename Traits>
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline auto
+HybridDirectMapImpl<Key, Value, Traits>::find(Key key) const
+    ABSL_ATTRIBUTE_LIFETIME_BOUND -> const_iterator {
+  RIEGELI_ASSERT(!direct_map_.get_deleter().IsMovedFromIfNull() ||
+                 direct_map_ != nullptr)
+      << "Moved-from HybridDirectMap";
+  const RawKey raw_key = Traits::ToRawKey(key);
+  if (raw_key < direct_map_.get_deleter().size()) {
+    if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) {
+      return const_iterator(this,
+                            direct_map_[raw_key] == nullptr
+                                ? 0
+                                : direct_map_.get_deleter().size() - raw_key);
+    }
+    if (direct_map_[raw_key] == nullptr) {
+      return const_iterator(this, 0, slow_map_->cend());
+    }
+    return const_iterator(this, direct_map_.get_deleter().size() - raw_key,
+                          slow_map_->cbegin());
+  }
+  if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) return const_iterator(this, 0);
+  return const_iterator(this, 0, std::as_const(*slow_map_).find(raw_key));
+}
+
+template <typename Key, typename Value, typename Traits>
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline bool
+HybridDirectMapImpl<Key, Value, Traits>::contains(Key key) const {
+  RIEGELI_ASSERT(!direct_map_.get_deleter().IsMovedFromIfNull() ||
+                 direct_map_ != nullptr)
+      << "Moved-from HybridDirectMap";
+  const RawKey raw_key = Traits::ToRawKey(key);
+  if (raw_key < direct_map_.get_deleter().size()) {
+    return direct_map_[raw_key] != nullptr;
+  }
+  if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) return false;
+  return slow_map_->contains(raw_key);
+}
+
+template <typename Key, typename Value, typename Traits>
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline Value&
+HybridDirectMapImpl<Key, Value, Traits>::at(Key key)
+    ABSL_ATTRIBUTE_LIFETIME_BOUND {
+  return const_cast<Value&>(std::as_const(*this).at(key));
+}
+
+template <typename Key, typename Value, typename Traits>
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline const Value&
+HybridDirectMapImpl<Key, Value, Traits>::at(Key key) const
+    ABSL_ATTRIBUTE_LIFETIME_BOUND {
+  RIEGELI_ASSERT(!direct_map_.get_deleter().IsMovedFromIfNull() ||
+                 direct_map_ != nullptr)
+      << "Moved-from HybridDirectMap";
+  const RawKey raw_key = Traits::ToRawKey(key);
+  if (raw_key < direct_map_.get_deleter().size()) {
+    const Value* const absl_nullable value = direct_map_[raw_key];
+    if (ABSL_PREDICT_FALSE(value == nullptr)) KeyNotFound(key);
+    return *value;
+  }
+  if (ABSL_PREDICT_FALSE(slow_map_ == nullptr)) KeyNotFound(key);
+  const auto iter = slow_map_->find(raw_key);
+  if (ABSL_PREDICT_FALSE(iter == slow_map_->end())) KeyNotFound(key);
+  return iter->second;
+}
+
+template <typename Key, typename Value, typename Traits>
+ABSL_ATTRIBUTE_NORETURN void
+HybridDirectMapImpl<Key, Value, Traits>::KeyNotFound(Key key) {
+  RIEGELI_CHECK_UNREACHABLE()
+      << "HybridDirectMap key not found: " << riegeli::Debug(key);
+}
+
+template <typename Key, typename Value, typename Traits>
+inline size_t HybridDirectMapImpl<Key, Value, Traits>::FirstRawKey() const {
+  const size_t direct_map_size = direct_map_.get_deleter().size();
+  for (size_t raw_key = 0; raw_key < direct_map_size; ++raw_key) {
+    if (direct_map_[raw_key] != nullptr) return raw_key;
+  }
+  return direct_map_size;
+}
+
+template <typename Key, typename Value, typename Traits>
+inline size_t HybridDirectMapImpl<Key, Value, Traits>::size() const {
+  return direct_values_.get_deleter().size() +
+         (ABSL_PREDICT_TRUE(slow_map_ == nullptr) ? 0 : slow_map_->size());
+}
+
+template <typename Key, typename Value, typename Traits>
+inline auto HybridDirectMapImpl<Key, Value, Traits>::begin()
+    ABSL_ATTRIBUTE_LIFETIME_BOUND -> iterator {
+  const size_t raw_key_complement =
+      direct_map_.get_deleter().size() - FirstRawKey();
+  if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) {
+    return iterator(this, raw_key_complement);
+  }
+  return iterator(this, raw_key_complement, slow_map_->begin());
+}
+
+template <typename Key, typename Value, typename Traits>
+inline auto HybridDirectMapImpl<Key, Value, Traits>::begin() const
+    ABSL_ATTRIBUTE_LIFETIME_BOUND -> const_iterator {
+  const size_t raw_key_complement =
+      direct_map_.get_deleter().size() - FirstRawKey();
+  if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) {
+    return const_iterator(this, raw_key_complement);
+  }
+  return const_iterator(this, raw_key_complement, slow_map_->cbegin());
+}
+
+template <typename Key, typename Value, typename Traits>
+inline auto HybridDirectMapImpl<Key, Value, Traits>::end()
+    ABSL_ATTRIBUTE_LIFETIME_BOUND -> iterator {
+  if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) return iterator(this, 0);
+  return iterator(this, 0, slow_map_->end());
+}
+
+template <typename Key, typename Value, typename Traits>
+inline auto HybridDirectMapImpl<Key, Value, Traits>::end() const
+    ABSL_ATTRIBUTE_LIFETIME_BOUND -> const_iterator {
+  if (ABSL_PREDICT_TRUE(slow_map_ == nullptr)) return const_iterator(this, 0);
+  return const_iterator(this, 0, slow_map_->cend());
+}
+
+template <typename Key, typename Value, typename Traits>
+bool HybridDirectMapImpl<Key, Value, Traits>::Equal(
+    const HybridDirectMapImpl& a, const HybridDirectMapImpl& b) {
+  if (a.size() != b.size()) return false;
+  const HybridDirectMapImpl* outer;
+  const HybridDirectMapImpl* inner;
+  if (a.capacity() <= b.capacity()) {
+    outer = &a;
+    inner = &b;
+  } else {
+    outer = &b;
+    inner = &a;
+  }
+  for (const_reference entry : *outer) {
+    const auto* const found = inner->FindOrNull(entry.first);
+    if (found == nullptr || *found != entry.second) return false;
+  }
+  return true;
+}
+
 }  // namespace hybrid_direct_internal
 
 }  // namespace riegeli
diff --git a/riegeli/messages/field_handler_map.h b/riegeli/messages/field_handler_map.h
index b6026e5..59ca566 100644
--- a/riegeli/messages/field_handler_map.h
+++ b/riegeli/messages/field_handler_map.h
@@ -94,7 +94,7 @@
   ABSL_ATTRIBUTE_ALWAYS_INLINE
   const FieldAction<uint64_t>* absl_nullable AcceptVarint(
       int field_number) const {
-    return varint_handlers_.Find(field_number);
+    return varint_handlers_.FindOrNull(field_number);
   }
 
   absl::Status DynamicHandleVarint(const FieldAction<uint64_t>& handler,
@@ -105,7 +105,7 @@
   ABSL_ATTRIBUTE_ALWAYS_INLINE
   const FieldAction<uint32_t>* absl_nullable AcceptFixed32(
       int field_number) const {
-    return fixed32_handlers_.Find(field_number);
+    return fixed32_handlers_.FindOrNull(field_number);
   }
 
   absl::Status DynamicHandleFixed32(const FieldAction<uint32_t>& handler,
@@ -116,7 +116,7 @@
   ABSL_ATTRIBUTE_ALWAYS_INLINE
   const FieldAction<uint64_t>* absl_nullable AcceptFixed64(
       int field_number) const {
-    return fixed64_handlers_.Find(field_number);
+    return fixed64_handlers_.FindOrNull(field_number);
   }
 
   absl::Status DynamicHandleFixed64(const FieldAction<uint64_t>& handler,
@@ -127,7 +127,7 @@
   ABSL_ATTRIBUTE_ALWAYS_INLINE
   const LengthDelimitedActions* absl_nullable AcceptLengthDelimited(
       int field_number) const {
-    return length_delimited_handlers_.Find(field_number);
+    return length_delimited_handlers_.FindOrNull(field_number);
   }
 
   absl::Status DynamicHandleLengthDelimitedFromReader(
@@ -150,7 +150,7 @@
 
   ABSL_ATTRIBUTE_ALWAYS_INLINE
   const FieldAction<>* absl_nullable AcceptStartGroup(int field_number) const {
-    return start_group_handlers_.Find(field_number);
+    return start_group_handlers_.FindOrNull(field_number);
   }
 
   absl::Status DynamicHandleStartGroup(const FieldAction<>& handler,
@@ -160,7 +160,7 @@
 
   ABSL_ATTRIBUTE_ALWAYS_INLINE
   const FieldAction<>* absl_nullable AcceptEndGroup(int field_number) const {
-    return end_group_handlers_.Find(field_number);
+    return end_group_handlers_.FindOrNull(field_number);
   }
 
   absl::Status DynamicHandleEndGroup(const FieldAction<>& handler,