Add `{,Dynamic,Unbound}FieldSkipper()`.

It becomes increasingly common to do nothing in a field handler besides tracking
field presence. It is annoying to write lambdas which takes the value, context
parameters, and just returns `absl::OkStatus()`, and it feels uncertain whether
taking and ignoring the value is as efficient as it could (it almost is;
arranging for this to perform varint skipping instead of varint parsing is not
worth complicating the field handler protocol).

In contrast to `DynamicFieldCopier()`, `DynamicFieldSkipper()` field predicate
returns `bool` instead of `std::optional<int>`, because returning a destination
field number is not applicable.

In contrast to `AnyFieldCopier()`, `AnyFieldSkipper()` is not provided because
skipping remaining fields is the implicit default.

In contrast to `UnboundFieldCopier()`, `UnboundFieldSkipper()` can reuse the
implementation of `FieldSkipper()`, because it does not need a destination field
number.

Also, make public `FieldAccepted` and `FieldAcceptedIf` for dynamic field
handlers which carry no extra information about the field being accepted. The
field handler protocol requires an API which fits `std::unique_ptr` and
`std::optional` but not `bool`. These types are extracted from a private
implementation detail of `DynamicFieldHandler()`. They are used by
`DynamicFieldSkipper()` too.

Also, fix embarrassing compile errors in `FieldHandlerWrapper()` for dynamic
field handlers.

PiperOrigin-RevId: 914263217
diff --git a/riegeli/messages/BUILD b/riegeli/messages/BUILD
index 459a3ae..90338f2 100644
--- a/riegeli/messages/BUILD
+++ b/riegeli/messages/BUILD
@@ -237,6 +237,21 @@
 )
 
 cc_library(
+    name = "field_skipper",
+    hdrs = ["field_skipper.h"],
+    deps = [
+        ":message_wire_format",
+        ":serialized_message_reader",
+        "//riegeli/base:cord_iterator_span",
+        "//riegeli/bytes:limiting_reader",
+        "@com_google_absl//absl/base:core_headers",
+        "@com_google_absl//absl/base:nullability",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings:string_view",
+    ],
+)
+
+cc_library(
     name = "field_copier",
     hdrs = ["field_copier.h"],
     deps = [
diff --git a/riegeli/messages/dynamic_field_handler.h b/riegeli/messages/dynamic_field_handler.h
index 7919f68..109387f 100644
--- a/riegeli/messages/dynamic_field_handler.h
+++ b/riegeli/messages/dynamic_field_handler.h
@@ -53,23 +53,6 @@
 // Implementation details follow.
 template <typename BaseFieldHandler>
 class DynamicFieldHandlerType {
- private:
-  struct Accepted {};
-
-  class MaybeAccepted {
-   public:
-    explicit MaybeAccepted(bool accepted) : accepted_(accepted) {}
-
-    MaybeAccepted(const MaybeAccepted& that) = default;
-
-    explicit operator bool() const { return accepted_; }
-
-    Accepted operator*() const { return Accepted(); }
-
-   private:
-    bool accepted_;
-  };
-
  public:
   template <
       typename BaseFieldHandlerInitializer,
@@ -87,8 +70,8 @@
   static constexpr int kFieldNumber = kDynamicFieldNumber;
 
   ABSL_ATTRIBUTE_ALWAYS_INLINE
-  MaybeAccepted AcceptVarint(int field_number) const {
-    return MaybeAccepted(field_number == field_number_);
+  FieldAcceptedIf AcceptVarint(int field_number) const {
+    return FieldAcceptedIf(field_number == field_number_);
   }
 
   template <
@@ -100,14 +83,14 @@
               serialized_message_reader_internal::IsStaticFieldHandlerForVarint<
                   BaseFieldHandler, Context...>>,
           int> = 0>
-  absl::Status DynamicHandleVarint(Accepted, uint64_t repr,
+  absl::Status DynamicHandleVarint(FieldAccepted, uint64_t repr,
                                    Context&... context) const {
     return base_field_handler_.HandleVarint(repr, context...);
   }
 
   ABSL_ATTRIBUTE_ALWAYS_INLINE
-  MaybeAccepted AcceptFixed32(int field_number) const {
-    return MaybeAccepted(field_number == field_number_);
+  FieldAcceptedIf AcceptFixed32(int field_number) const {
+    return FieldAcceptedIf(field_number == field_number_);
   }
 
   template <
@@ -119,14 +102,14 @@
               serialized_message_reader_internal::
                   IsStaticFieldHandlerForFixed32<BaseFieldHandler, Context...>>,
           int> = 0>
-  absl::Status DynamicHandleFixed32(Accepted, uint32_t repr,
+  absl::Status DynamicHandleFixed32(FieldAccepted, uint32_t repr,
                                     Context&... context) const {
     return base_field_handler_.HandleFixed32(repr, context...);
   }
 
   ABSL_ATTRIBUTE_ALWAYS_INLINE
-  MaybeAccepted AcceptFixed64(int field_number) const {
-    return MaybeAccepted(field_number == field_number_);
+  FieldAcceptedIf AcceptFixed64(int field_number) const {
+    return FieldAcceptedIf(field_number == field_number_);
   }
 
   template <
@@ -138,14 +121,14 @@
               serialized_message_reader_internal::
                   IsStaticFieldHandlerForFixed64<BaseFieldHandler, Context...>>,
           int> = 0>
-  absl::Status DynamicHandleFixed64(Accepted, uint64_t repr,
+  absl::Status DynamicHandleFixed64(FieldAccepted, uint64_t repr,
                                     Context&... context) const {
     return base_field_handler_.HandleFixed64(repr, context...);
   }
 
-  ABSL_ATTRIBUTE_ALWAYS_INLINE MaybeAccepted
+  ABSL_ATTRIBUTE_ALWAYS_INLINE FieldAcceptedIf
   AcceptLengthDelimited(int field_number) const {
-    return MaybeAccepted(field_number == field_number_);
+    return FieldAcceptedIf(field_number == field_number_);
   }
 
   template <typename... Context,
@@ -158,7 +141,7 @@
                             BaseFieldHandler, Context...>>,
                 int> = 0>
   absl::Status DynamicHandleLengthDelimitedFromReader(
-      Accepted, ReaderSpan<> repr, Context&... context) const {
+      FieldAccepted, ReaderSpan<> repr, Context&... context) const {
     return base_field_handler_.HandleLengthDelimitedFromReader(std::move(repr),
                                                                context...);
   }
@@ -172,7 +155,7 @@
                         IsStaticFieldHandlerForLengthDelimitedFromCord<
                             BaseFieldHandler, Context...>>,
                 int> = 0>
-  absl::Status DynamicHandleLengthDelimitedFromCord(Accepted,
+  absl::Status DynamicHandleLengthDelimitedFromCord(FieldAccepted,
                                                     CordIteratorSpan repr,
                                                     std::string& scratch,
                                                     Context&... context) const {
@@ -190,14 +173,14 @@
                             BaseFieldHandler, Context...>>,
                 int> = 0>
   absl::Status DynamicHandleLengthDelimitedFromString(
-      Accepted, absl::string_view repr, Context&... context) const {
+      FieldAccepted, absl::string_view repr, Context&... context) const {
     return base_field_handler_.HandleLengthDelimitedFromString(repr,
                                                                context...);
   }
 
   ABSL_ATTRIBUTE_ALWAYS_INLINE
-  MaybeAccepted AcceptStartGroup(int field_number) const {
-    return MaybeAccepted(field_number == field_number_);
+  FieldAcceptedIf AcceptStartGroup(int field_number) const {
+    return FieldAcceptedIf(field_number == field_number_);
   }
 
   template <typename... Context,
@@ -209,13 +192,14 @@
                         IsStaticFieldHandlerForStartGroup<BaseFieldHandler,
                                                           Context...>>,
                 int> = 0>
-  absl::Status DynamicHandleStartGroup(Accepted, Context&... context) const {
+  absl::Status DynamicHandleStartGroup(FieldAccepted,
+                                       Context&... context) const {
     return base_field_handler_.HandleStartGroup(context...);
   }
 
   ABSL_ATTRIBUTE_ALWAYS_INLINE
-  MaybeAccepted AcceptEndGroup(int field_number) const {
-    return MaybeAccepted(field_number == field_number_);
+  FieldAcceptedIf AcceptEndGroup(int field_number) const {
+    return FieldAcceptedIf(field_number == field_number_);
   }
 
   template <typename... Context,
@@ -227,7 +211,7 @@
                         IsStaticFieldHandlerForEndGroup<BaseFieldHandler,
                                                         Context...>>,
                 int> = 0>
-  absl::Status DynamicHandleEndGroup(Accepted, Context&... context) const {
+  absl::Status DynamicHandleEndGroup(FieldAccepted, Context&... context) const {
     return base_field_handler_.HandleEndGroup(context...);
   }
 
diff --git a/riegeli/messages/field_skipper.h b/riegeli/messages/field_skipper.h
new file mode 100644
index 0000000..4330706
--- /dev/null
+++ b/riegeli/messages/field_skipper.h
@@ -0,0 +1,321 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef RIEGELI_MESSAGES_FIELD_SKIPPER_H_
+#define RIEGELI_MESSAGES_FIELD_SKIPPER_H_
+
+#include <stdint.h>
+
+#include <string>
+#include <type_traits>
+#include <utility>
+
+#include "absl/base/attributes.h"
+#include "absl/base/nullability.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "riegeli/base/cord_iterator_span.h"
+#include "riegeli/bytes/limiting_reader.h"
+#include "riegeli/messages/message_wire_format.h"
+#include "riegeli/messages/serialized_message_reader.h"
+
+ABSL_POINTERS_DEFAULT_NONNULL
+
+namespace riegeli {
+
+// The type returned by `FieldSkipper()` and `UnboundFieldSkipper()`.
+template <int field_number, WireTypeSet wire_types = AllWireTypes>
+class FieldSkipperType;
+
+// The type returned by `DynamicFieldSkipper()`.
+template <typename Accept, WireTypeSet wire_types = AllWireTypes>
+class DynamicFieldSkipperType;
+
+// A field handler for `SerializedMessageReader` which ignores the given field.
+//
+// This can be used together with `FieldTracker()` or
+// `OnlyFirstFieldThenCancel()` to detect field presence without otherwise
+// processing its value.
+//
+// As an optimization, `wire_types` constrains the set of wire types to handle.
+// This yields smaller and faster code.
+template <int field_number, WireTypeSet wire_types = AllWireTypes>
+constexpr FieldSkipperType<field_number, wire_types> FieldSkipper() {
+  return FieldSkipperType<field_number, wire_types>();
+}
+
+// A field handler for `SerializedMessageReader` which ignores the given field,
+// with the predicate over field numbers specified at runtime.
+//
+// This can be used together with `FieldTracker()` or
+// `OnlyFirstFieldThenCancel()` to detect field presence without otherwise
+// processing its value.
+//
+// `accept` is an invocable taking the field number as `int` and returning
+// `bool`. If it returns `true`, the field is ignored.
+template <WireTypeSet wire_types = AllWireTypes, typename Accept>
+constexpr DynamicFieldSkipperType<std::decay_t<Accept>, wire_types>
+DynamicFieldSkipper(Accept&& accept) {
+  return DynamicFieldSkipperType<std::decay_t<Accept>, wire_types>(
+      std::forward<Accept>(accept));
+}
+
+// An unbound field handler for a `DynamicFieldHandler` or `FieldHandlerMap`
+// which ignores the current field, with the field number specified at runtime.
+//
+// This can be used together with `FieldTracker()` or
+// `OnlyFirstFieldThenCancel()` to detect field presence without otherwise
+// processing its value.
+//
+// As an optimization, `wire_types` constrains the set of wire types to handle.
+// This yields smaller and faster code.
+template <WireTypeSet wire_types = AllWireTypes>
+constexpr FieldSkipperType<kUnboundFieldNumber, wire_types>
+UnboundFieldSkipper() {
+  return FieldSkipperType<kUnboundFieldNumber, wire_types>();
+}
+
+// Implementation details follow.
+
+template <int field_number, WireTypeSet wire_types>
+class FieldSkipperType {
+ public:
+  static constexpr int kFieldNumber = field_number;
+
+  constexpr FieldSkipperType() = default;
+
+  FieldSkipperType(const FieldSkipperType& that) = default;
+  FieldSkipperType& operator=(const FieldSkipperType& that) = default;
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kVarint) ==
+                                 WireTypeSet::kVarint,
+                             int> = 0>
+  absl::Status HandleVarint(uint64_t /*repr*/, Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kFixed32) ==
+                                 WireTypeSet::kFixed32,
+                             int> = 0>
+  absl::Status HandleFixed32(uint32_t /*repr*/, Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kFixed64) ==
+                                 WireTypeSet::kFixed64,
+                             int> = 0>
+  absl::Status HandleFixed64(uint64_t /*repr*/, Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <
+      typename... Context, WireTypeSet dependent_wire_types = wire_types,
+      std::enable_if_t<(dependent_wire_types & WireTypeSet::kLengthDelimited) ==
+                           WireTypeSet::kLengthDelimited,
+                       int> = 0>
+  absl::Status HandleLengthDelimitedFromReader(ReaderSpan<> repr,
+                                               Context&... /*context*/) const {
+    return riegeli::SkipLengthDelimitedFromReader(std::move(repr));
+  }
+
+  template <
+      typename... Context, WireTypeSet dependent_wire_types = wire_types,
+      std::enable_if_t<(dependent_wire_types & WireTypeSet::kLengthDelimited) ==
+                           WireTypeSet::kLengthDelimited,
+                       int> = 0>
+  absl::Status HandleLengthDelimitedFromCord(CordIteratorSpan repr,
+                                             std::string& /*scratch*/,
+                                             Context&... /*context*/) const {
+    return riegeli::SkipLengthDelimitedFromCord(std::move(repr));
+  }
+
+  template <
+      typename... Context, WireTypeSet dependent_wire_types = wire_types,
+      std::enable_if_t<(dependent_wire_types & WireTypeSet::kLengthDelimited) ==
+                           WireTypeSet::kLengthDelimited,
+                       int> = 0>
+  absl::Status HandleLengthDelimitedFromString(absl::string_view /*repr*/,
+                                               Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <
+      typename... Context, WireTypeSet dependent_wire_types = wire_types,
+      std::enable_if_t<(dependent_wire_types & WireTypeSet::kStartGroup) ==
+                           WireTypeSet::kStartGroup,
+                       int> = 0>
+  absl::Status HandleStartGroup(Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kEndGroup) ==
+                                 WireTypeSet::kEndGroup,
+                             int> = 0>
+  absl::Status HandleEndGroup(Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+};
+
+template <typename Accept, WireTypeSet wire_types>
+class DynamicFieldSkipperType {
+ public:
+  static constexpr int kFieldNumber = kDynamicFieldNumber;
+
+  template <typename AcceptInitializer,
+            std::enable_if_t<std::is_convertible_v<AcceptInitializer&&, Accept>,
+                             int> = 0>
+  explicit constexpr DynamicFieldSkipperType(AcceptInitializer&& accept)
+      : accept_(std::forward<AcceptInitializer>(accept)) {}
+
+  DynamicFieldSkipperType() = default;
+
+  DynamicFieldSkipperType(const DynamicFieldSkipperType& that) = default;
+  DynamicFieldSkipperType& operator=(const DynamicFieldSkipperType& that) =
+      default;
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kVarint) ==
+                                 WireTypeSet::kVarint,
+                             int> = 0>
+  FieldAcceptedIf AcceptVarint(int field_number) const {
+    return FieldAcceptedIf(accept_(field_number));
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kVarint) ==
+                                 WireTypeSet::kVarint,
+                             int> = 0>
+  absl::Status DynamicHandleVarint(FieldAccepted, uint64_t /*repr*/,
+                                   Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kFixed32) ==
+                                 WireTypeSet::kFixed32,
+                             int> = 0>
+  FieldAcceptedIf AcceptFixed32(int field_number) const {
+    return FieldAcceptedIf(accept_(field_number));
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kFixed32) ==
+                                 WireTypeSet::kFixed32,
+                             int> = 0>
+  absl::Status DynamicHandleFixed32(FieldAccepted, uint32_t /*repr*/,
+                                    Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kFixed64) ==
+                                 WireTypeSet::kFixed64,
+                             int> = 0>
+  FieldAcceptedIf AcceptFixed64(int field_number) const {
+    return FieldAcceptedIf(accept_(field_number));
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kFixed64) ==
+                                 WireTypeSet::kFixed64,
+                             int> = 0>
+  absl::Status DynamicHandleFixed64(FieldAccepted, uint64_t /*repr*/,
+                                    Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <typename... Context>
+  FieldAcceptedIf AcceptLengthDelimited(int field_number) const {
+    return FieldAcceptedIf(accept_(field_number));
+  }
+
+  template <
+      typename... Context, WireTypeSet dependent_wire_types = wire_types,
+      std::enable_if_t<(dependent_wire_types & WireTypeSet::kLengthDelimited) ==
+                           WireTypeSet::kLengthDelimited,
+                       int> = 0>
+  absl::Status DynamicHandleLengthDelimitedFromReader(
+      FieldAccepted, ReaderSpan<> repr, Context&... /*context*/) const {
+    return riegeli::SkipLengthDelimitedFromReader(std::move(repr));
+  }
+
+  template <
+      typename... Context, WireTypeSet dependent_wire_types = wire_types,
+      std::enable_if_t<(dependent_wire_types & WireTypeSet::kLengthDelimited) ==
+                           WireTypeSet::kLengthDelimited,
+                       int> = 0>
+  absl::Status DynamicHandleLengthDelimitedFromCord(
+      FieldAccepted, CordIteratorSpan repr, std::string& /*scratch*/,
+      Context&... /*context*/) const {
+    return riegeli::SkipLengthDelimitedFromCord(std::move(repr));
+  }
+
+  template <
+      typename... Context, WireTypeSet dependent_wire_types = wire_types,
+      std::enable_if_t<(dependent_wire_types & WireTypeSet::kLengthDelimited) ==
+                           WireTypeSet::kLengthDelimited,
+                       int> = 0>
+  absl::Status DynamicHandleLengthDelimitedFromString(
+      FieldAccepted, absl::string_view /*repr*/,
+      Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <
+      typename... Context, WireTypeSet dependent_wire_types = wire_types,
+      std::enable_if_t<(dependent_wire_types & WireTypeSet::kStartGroup) ==
+                           WireTypeSet::kStartGroup,
+                       int> = 0>
+  FieldAcceptedIf AcceptStartGroup(int field_number) const {
+    return FieldAcceptedIf(accept_(field_number));
+  }
+
+  template <
+      typename... Context, WireTypeSet dependent_wire_types = wire_types,
+      std::enable_if_t<(dependent_wire_types & WireTypeSet::kStartGroup) ==
+                           WireTypeSet::kStartGroup,
+                       int> = 0>
+  absl::Status DynamicHandleStartGroup(FieldAccepted,
+                                       Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kEndGroup) ==
+                                 WireTypeSet::kEndGroup,
+                             int> = 0>
+  FieldAcceptedIf AcceptEndGroup(int field_number) const {
+    return FieldAcceptedIf(accept_(field_number));
+  }
+
+  template <typename... Context, WireTypeSet dependent_wire_types = wire_types,
+            std::enable_if_t<(dependent_wire_types & WireTypeSet::kEndGroup) ==
+                                 WireTypeSet::kEndGroup,
+                             int> = 0>
+  absl::Status DynamicHandleEndGroup(FieldAccepted,
+                                     Context&... /*context*/) const {
+    return absl::OkStatus();
+  }
+
+ private:
+  ABSL_ATTRIBUTE_NO_UNIQUE_ADDRESS Accept accept_;
+};
+
+}  // namespace riegeli
+
+#endif  // RIEGELI_MESSAGES_FIELD_SKIPPER_H_
diff --git a/riegeli/messages/fields_found.h b/riegeli/messages/fields_found.h
index b10c899..e846bfe 100644
--- a/riegeli/messages/fields_found.h
+++ b/riegeli/messages/fields_found.h
@@ -476,8 +476,8 @@
                                    Context&... context) const {
     return action_wrapper_(
         [&] {
-          return field_handler_.HandleVarint(std::forward<Accepted>(accepted),
-                                             repr, context...);
+          return field_handler_.DynamicHandleVarint(
+              std::forward<Accepted>(accepted), repr, context...);
         },
         [] { return absl::OkStatus(); }, context...);
   }
@@ -513,8 +513,8 @@
                                     Context&... context) const {
     return action_wrapper_(
         [&] {
-          return field_handler_.HandleFixed32(std::forward<Accepted>(accepted),
-                                              repr, context...);
+          return field_handler_.DynamicHandleFixed32(
+              std::forward<Accepted>(accepted), repr, context...);
         },
         [] { return absl::OkStatus(); }, context...);
   }
@@ -550,8 +550,8 @@
                                     Context&... context) const {
     return action_wrapper_(
         [&] {
-          return field_handler_.HandleFixed64(std::forward<Accepted>(accepted),
-                                              repr, context...);
+          return field_handler_.DynamicHandleFixed64(
+              std::forward<Accepted>(accepted), repr, context...);
         },
         [] { return absl::OkStatus(); }, context...);
   }
@@ -626,7 +626,7 @@
       Accepted&& accepted, ReaderSpan<> repr, Context&... context) const {
     return action_wrapper_(
         [&] {
-          return field_handler_.HandleLengthDelimitedFromReader(
+          return field_handler_.DynamicHandleLengthDelimitedFromReader(
               std::forward<Accepted>(accepted), std::move(repr), context...);
         },
         [&] { return SkipLengthDelimitedFromReader(std::move(repr)); },
@@ -645,7 +645,7 @@
                                                     Context&... context) const {
     return action_wrapper_(
         [&] {
-          return field_handler_.HandleLengthDelimitedFromCord(
+          return field_handler_.DynamicHandleLengthDelimitedFromCord(
               std::forward<Accepted>(accepted), std::move(repr), scratch,
               context...);
         },
@@ -663,7 +663,7 @@
       Accepted&& accepted, absl::string_view repr, Context&... context) const {
     return action_wrapper_(
         [&] {
-          return field_handler_.HandleLengthDelimitedFromString(
+          return field_handler_.DynamicHandleLengthDelimitedFromString(
               std::forward<Accepted>(accepted), repr, context...);
         },
         [] { return absl::OkStatus(); }, context...);
@@ -699,7 +699,7 @@
                                        Context&... context) const {
     return action_wrapper_(
         [&] {
-          return field_handler_.HandleStartGroup(
+          return field_handler_.DynamicHandleStartGroup(
               std::forward<Accepted>(accepted), context...);
         },
         [] { return absl::OkStatus(); }, context...);
@@ -736,8 +736,8 @@
                                      Context&... context) const {
     return action_wrapper_(
         [&] {
-          return field_handler_.HandleEndGroup(std::forward<Accepted>(accepted),
-                                               context...);
+          return field_handler_.DynamicHandleEndGroup(
+              std::forward<Accepted>(accepted), context...);
         },
         [] { return absl::OkStatus(); }, context...);
   }
diff --git a/riegeli/messages/serialized_message_reader.h b/riegeli/messages/serialized_message_reader.h
index 510abf4..9b06801 100644
--- a/riegeli/messages/serialized_message_reader.h
+++ b/riegeli/messages/serialized_message_reader.h
@@ -228,10 +228,10 @@
 //
 // `MaybeAccepted` is some type explicitly convertible to `bool`, with
 // `operator*` returning some `Accepted` type. `MaybeAccepted` can be e.g.
-// `std::optional<Accepted>` or `Accepted*`. If `AcceptX()` returns a value
-// explicitly convertible to `true`, then the field is accepted, and the
-// corresponding `HandleX()` function is called with the result of `operator*`
-// as the first argument.
+// `std::optional<Accepted>`, `Accepted*`, or `FieldAcceptedIf`. If `AcceptX()`
+// returns a value explicitly convertible to `true`, then the field is accepted,
+// and the corresponding `HandleX()` function is called with the result of
+// `operator*` as the first argument.
 
 // In `FieldHandler::kFieldNumber`, marks a dynamic field handler.
 inline constexpr int kDynamicFieldNumber =
@@ -572,6 +572,31 @@
 
 absl::Status SkipLengthDelimitedFromCord(CordIteratorSpan value);
 
+// An `Accepted` type for a dynamic field handler which does not carry any
+// information about the field being accepted.
+//
+// This is used as the first parameter of a `DynamicHandleX()` function.
+struct FieldAccepted {};
+
+// A `MaybeAccepted` type for a dynamic field handler which does not carry any
+// information about the field being accepted.
+//
+// This is used as the result of an `AcceptX()` function.
+class FieldAcceptedIf {
+ public:
+  explicit FieldAcceptedIf(bool accepted) : accepted_(accepted) {}
+
+  FieldAcceptedIf(const FieldAcceptedIf& that) = default;
+  FieldAcceptedIf& operator=(const FieldAcceptedIf& that) = default;
+
+  explicit operator bool() const { return accepted_; }
+
+  FieldAccepted operator*() const { return FieldAccepted(); }
+
+ private:
+  bool accepted_;
+};
+
 // Implementation details follow.
 
 template <typename T, typename... Context>