diff --git a/CMakeLists.txt b/CMakeLists.txt
index eaf9df6..bd84c96 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -222,11 +222,20 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT)
protocol_reflection_detail/thunk.hxx
protocol_reflection_detail/thunk_test.cc
protocol_reflection_detail/conformance.hxx
- protocol_reflection_detail/conformance_test.cc)
+ protocol_reflection_detail/conformance_test.cc
+ protocol_reflection_detail/forwarders.hxx
+ protocol_reflection_detail/forwarders_test.cc)
target_compile_options(protocol_reflection_detail_test
PRIVATE -freflection)
target_include_directories(protocol_reflection_detail_test
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+ # GCC 16 trunk cannot stream std::meta::info-typed constants through
+ # LTO ("internal compiler error: tree code 'meta_type' is not
+ # supported in LTO streams"), which forwarders.hxx's substitute-built
+ # base-class lists trigger; this target is a test binary with no need
+ # for LTO's runtime performance benefit.
+ set_target_properties(protocol_reflection_detail_test PROPERTIES
+ INTERPROCEDURAL_OPTIMIZATION OFF)
endif()
add_executable(protocol_benchmark protocol_benchmark.cc)
diff --git a/protocol_reflection_detail/forwarders.hxx b/protocol_reflection_detail/forwarders.hxx
new file mode 100644
index 0000000..cfe6111
--- /dev/null
+++ b/protocol_reflection_detail/forwarders.hxx
@@ -0,0 +1,72 @@
+/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+==============================================================================*/
+// Generalizes a one-member forwarder base to a reflection-computed number
+// of members. A Wrapper here recovers its owner by casting through the
+// one-member base forwarder_base gives it, then a base-to-derived
+// static_cast to the owner. This file only assembles the bases; a
+// Wrapper's own behavior and its owner's type are defined elsewhere, by
+// whatever combines these bases into a concrete forwarding type.
+#ifndef XYZ_PROTOCOL_REFLECTION_DETAIL_FORWARDERS_HXX_
+#define XYZ_PROTOCOL_REFLECTION_DETAIL_FORWARDERS_HXX_
+
+#include
+#include
+
+#include "protocol_reflection_detail/naming.hxx"
+
+namespace xyz::reflection_detail {
+
+// A struct equivalent to (for e.g. Wrapper = GreetWrapper, Member =
+// ^^Greeter::name):
+// struct Incomplete { [[no_unique_address]] GreetWrapper name; };
+// Member is a template parameter rather than a function parameter because
+// consteval blocks can't use local variables or parameters, only template
+// parameters, and define_aggregate must run from one.
+template
+consteval std::meta::info make_forwarder_base_info() {
+ struct Incomplete;
+ consteval {
+ define_aggregate(^^Incomplete,
+ {
+ std::meta::data_member_spec(
+ ^^Wrapper, {
+ .name = vtable_entry_name(Member),
+ .no_unique_address = true})});
+ }
+ return ^^Incomplete;
+}
+
+template
+using forwarder_base = typename[:make_forwarder_base_info():];
+
+// Combines N forwarder_base specializations into one type via ordinary
+// multiple inheritance, giving their eventual owner every named forwarder
+// at once with no splicing at any call site.
+template
+struct forwarders : Bases... {};
+
+consteval std::meta::info forwarders_type(
+ const std::vector& base_types) {
+ return std::meta::substitute(^^forwarders, base_types);
+}
+
+} // namespace xyz::reflection_detail
+
+#endif // XYZ_PROTOCOL_REFLECTION_DETAIL_FORWARDERS_HXX_
diff --git a/protocol_reflection_detail/forwarders_test.cc b/protocol_reflection_detail/forwarders_test.cc
new file mode 100644
index 0000000..cc4f4c4
--- /dev/null
+++ b/protocol_reflection_detail/forwarders_test.cc
@@ -0,0 +1,156 @@
+/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+==============================================================================*/
+#include "protocol_reflection_detail/forwarders.hxx"
+
+#include
+
+#include
+#include
+#include
+
+namespace {
+
+using xyz::reflection_detail::forwarder_base;
+using xyz::reflection_detail::forwarders_type;
+
+// Names only: an interface never instantiated, just reflected upon so
+// forwarder_base has something to name each synthesized base after.
+struct NamingFixture {
+ std::string greet(std::string_view) const;
+ std::string shout(std::string_view) const;
+ int count() const;
+};
+
+// --- Three members: GreetBase/ShoutBase/CountBase synthesized via
+// forwarder_base instead of hand-written.
+
+struct ThreeMemberOwner;
+
+struct GreetWrapper {
+ std::string operator()(std::string_view visitor) const;
+};
+
+struct ShoutWrapper {
+ std::string operator()(std::string_view visitor) const;
+};
+
+struct CountWrapper {
+ int operator()() const;
+};
+
+using GreetBase = forwarder_base;
+using ShoutBase = forwarder_base;
+using CountBase = forwarder_base;
+
+using ThreeMemberForwarders =
+ typename[:forwarders_type({^^GreetBase, ^^ShoutBase, ^^CountBase}):];
+
+struct ThreeMemberOwner : ThreeMemberForwarders {
+ std::string name = "World";
+};
+
+std::string GreetWrapper::operator()(std::string_view visitor) const {
+ const auto* base =
+ static_cast(static_cast(this));
+ const auto* owner = static_cast(base);
+ return "Hello, " + std::string(visitor) + ", from " + owner->name + "!";
+}
+
+std::string ShoutWrapper::operator()(std::string_view visitor) const {
+ const auto* base =
+ static_cast(static_cast(this));
+ const auto* owner = static_cast(base);
+ return std::string(visitor) + ", " + owner->name + " SHOUTS HELLO!";
+}
+
+int CountWrapper::operator()() const {
+ const auto* base =
+ static_cast(static_cast(this));
+ const auto* owner = static_cast(base);
+ return static_cast(owner->name.size());
+}
+
+TEST(Forwarders, EachSynthesizedBaseIsARealBaseOfTheOwner) {
+ static_assert(std::is_base_of_v);
+ static_assert(std::is_base_of_v);
+ static_assert(std::is_base_of_v);
+}
+
+TEST(Forwarders, InheritedLookupCallsEachNamedForwarderWithNoSplicing) {
+ ThreeMemberOwner owner;
+ owner.name = "Owner";
+
+ EXPECT_EQ(owner.greet("Reader"), "Hello, Reader, from Owner!");
+ EXPECT_EQ(owner.shout("Reader"), "Reader, Owner SHOUTS HELLO!");
+ EXPECT_EQ(owner.count(), 5);
+}
+
+TEST(Forwarders, EmptyBasesAddNoStorageBeyondTheOwnersOwnMembers) {
+ // Three no_unique_address forwarder bases, none adding size: sizeof
+ // matches a bare std::string member.
+ EXPECT_EQ(sizeof(ThreeMemberOwner), sizeof(std::string));
+}
+
+// --- One member.
+
+struct OneMemberOwner;
+
+struct OneWrapper {
+ int operator()() const;
+};
+
+using OneBase = forwarder_base;
+using OneMemberForwarders = typename[:forwarders_type({^^OneBase}):];
+
+struct OneMemberOwner : OneMemberForwarders {
+ int value = 7;
+};
+
+int OneWrapper::operator()() const {
+ const auto* base =
+ static_cast(static_cast(this));
+ const auto* owner = static_cast(base);
+ return owner->value;
+}
+
+TEST(Forwarders, OneMemberIsABaseAndAddsNoStorage) {
+ static_assert(std::is_base_of_v);
+ static_assert(sizeof(OneMemberOwner) == sizeof(int));
+}
+
+TEST(Forwarders, OneMemberDispatchesCorrectly) {
+ OneMemberOwner owner;
+ EXPECT_EQ(owner.count(), 7);
+}
+
+// --- Zero members: forwarders<> is a valid, empty combinator.
+
+using ZeroMemberForwarders = typename[:forwarders_type({}):];
+
+struct ZeroMemberOwner : ZeroMemberForwarders {
+ int value = 0;
+};
+
+TEST(Forwarders, ZeroMembersIsAnEmptyTypeAddingNoStorage) {
+ static_assert(std::is_empty_v);
+ static_assert(sizeof(ZeroMemberOwner) == sizeof(int));
+}
+
+} // namespace