From 55c8bd6198be1b7bf2e8f25d6e75680369f33d57 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 13:01:30 +0100 Subject: [PATCH 01/11] Consolidate protocol library on manual vtable dispatch strategy for simplicity. --- CMakeLists.txt | 25 +- CONTRIBUTING.md | 12 +- README.md | 16 +- cmake/xyz_generate_protocol.cmake | 8 +- interface_benchmark.h | 20 -- protocol.h | 3 +- protocol_benchmark.cc | 146 ++------- scripts/cmake.py | 7 - scripts/protocol.j2 | 268 ++++++++-------- scripts/protocol_manual_vtable.j2 | 510 ------------------------------ scripts/test_generate_protocol.py | 8 +- 11 files changed, 172 insertions(+), 851 deletions(-) delete mode 100644 interface_benchmark.h delete mode 100644 scripts/protocol_manual_vtable.j2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 1729a6c..96e12d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,43 +98,26 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) enable_testing() - option(XYZ_PROTOCOL_GENERATE_MANUAL_VTABLE "Generate manual vtable template" OFF) - - set(OPT_MANUAL_VTABLE "") - if(XYZ_PROTOCOL_GENERATE_MANUAL_VTABLE) - set(OPT_MANUAL_VTABLE MANUAL_VTABLE) - endif() - xyz_generate_protocol( CLASS_NAME A INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_A.h HEADER interface_A.h - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A.h ${OPT_MANUAL_VTABLE}) - xyz_generate_protocol( - CLASS_NAME A_virtual INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_benchmark.h - HEADER interface_benchmark.h - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A_virtual.h) - xyz_generate_protocol( - CLASS_NAME A_manual INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_benchmark.h - HEADER interface_benchmark.h - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A_manual.h MANUAL_VTABLE) + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A.h) xyz_generate_protocol( CLASS_NAME B INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_B.h HEADER interface_B.h - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_B.h ${OPT_MANUAL_VTABLE}) + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_B.h) xyz_generate_protocol( CLASS_NAME C INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_C.h HEADER interface_C.h - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_C.h ${OPT_MANUAL_VTABLE}) + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_C.h) xyz_generate_protocol( CLASS_NAME D INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_D.h HEADER interface_D.h - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_D.h ${OPT_MANUAL_VTABLE}) + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_D.h) add_custom_target( generate_protocols DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A.h - ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A_virtual.h - ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A_manual.h ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_B.h ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_C.h ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_D.h) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f74bec0..4e77735 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,14 +109,6 @@ The `xyz_generate_protocol` CMake macro automates code generation and supports e OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_MyInterface.h" ) - # Generate explicit manual vtable-based protocol - xyz_generate_protocol( - CLASS_NAME MyInterface_manual - INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/MyInterface.h" - HEADER "MyInterface.h" - OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_MyInterface_manual.h" - MANUAL_VTABLE - ) ``` This macro ensures that the Python script runs during the CMake configuration @@ -124,10 +116,10 @@ The `xyz_generate_protocol` CMake macro automates code generation and supports e ## Performance and Benchmarks -The project builds both the standard virtual-dispatch protocol and the explicit `MANUAL_VTABLE` protocol implementation alongside each other to ensure behavior matches. You can directly compare the performance of member function dispatch, copying, and moving by running the `protocol_benchmark` target via the wrapper script: +You can measure the performance of member function dispatch, copying, and moving by running the `protocol_benchmark` target via the wrapper script: ```bash -./scripts/cmake.sh --benchmark +./scripts/cmake.sh benchmark ``` ## Usage Examples diff --git a/README.md b/README.md index bbe1057..ba7e7f0 100644 --- a/README.md +++ b/README.md @@ -153,22 +153,12 @@ lightweight indirection. ## Implementation Details and Benchmarks -The code generator supports two dispatch strategies: +The code generator generates a struct-of-function-pointers representing the vtable, managing type-erasure and dispatch via pointer indirection (manual vtables). This enforces constraints (value semantics, `const` correctness, and custom allocators) without requiring standard inheritance or compiler-generated virtual tables. -1. Virtual Dispatch (Default): Generates a traditional C++ polymorphic class - hierarchy with `virtual` methods. The type-erased wrapper heap-allocates a - control block derived from a common interface. - -2. Manual Vtables: Generates a struct-of-function-pointers representing the - vtable, managing type-erasure and dispatch via pointer indirection. - -Both implementations enforce identical constraints (value semantics, `const` -correctness, and custom allocators). The library builds both versions to verify -equivalence and provides a `protocol_benchmark` target for directly comparing -their performance across allocations, copies, moves, and member function calls. +The library provides a `protocol_benchmark` target for measuring the performance of the protocol across allocations, copies, moves, and member function calls. ```bash -# Build and run the benchmark comparing the two implementations +# Build and run the benchmark ./scripts/cmake.sh benchmark ``` diff --git a/cmake/xyz_generate_protocol.cmake b/cmake/xyz_generate_protocol.cmake index d760a5c..539fbc8 100644 --- a/cmake/xyz_generate_protocol.cmake +++ b/cmake/xyz_generate_protocol.cmake @@ -38,17 +38,13 @@ a Jinja2 template and a Python generation script. #]=======================================================================] macro(xyz_generate_protocol) - set(options MANUAL_VTABLE) + set(options "") set(oneValueArgs CLASS_NAME INTERFACE OUTPUT HEADER) set(multiValueArgs "") cmake_parse_arguments(XYZ_GENERATE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - if(XYZ_GENERATE_MANUAL_VTABLE) - set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/protocol_manual_vtable.j2) - else() - set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/protocol.j2) - endif() + set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/protocol.j2) get_filename_component(XYZ_GENERATE_OUTPUT_DIR "${XYZ_GENERATE_OUTPUT}" DIRECTORY) add_custom_command( diff --git a/interface_benchmark.h b/interface_benchmark.h deleted file mode 100644 index 495358a..0000000 --- a/interface_benchmark.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef XYZ_PROTOCOL_INTERFACE_BENCHMARK_H -#define XYZ_PROTOCOL_INTERFACE_BENCHMARK_H -#include - -namespace xyz { - -struct A_virtual { - std::string_view name() const { return "A_virtual"; } - - int count() { return 42; } -}; - -struct A_manual { - std::string_view name() const { return "A_manual"; } - - int count() { return 42; } -}; - -} // namespace xyz -#endif // XYZ_PROTOCOL_INTERFACE_BENCHMARK_H diff --git a/protocol.h b/protocol.h index deb5d59..17620a0 100644 --- a/protocol.h +++ b/protocol.h @@ -38,8 +38,7 @@ class protocol { "definition.\n" " HEADER: The name of the header file to be included in the generated " "file.\n" - " OUTPUT: The path where the generated header should be written.\n" - " MANUAL_VTABLE (optional): If set, use the manual vtable template.\n\n" + " OUTPUT: The path where the generated header should be written.\n\n" "Example usage:\n" " xyz_generate_protocol(\n" " CLASS_NAME MyInterface\n" diff --git a/protocol_benchmark.cc b/protocol_benchmark.cc index e5ded73..ef2c1d4 100644 --- a/protocol_benchmark.cc +++ b/protocol_benchmark.cc @@ -3,20 +3,19 @@ #include #include -#include "generated/protocol_A_manual.h" -#include "generated/protocol_A_virtual.h" -#include "interface_benchmark.h" +#include "generated/protocol_A.h" +#include "interface_A.h" namespace { struct ALike { - std::string_view name() const { return "ALike"; } + std::string_view name() const noexcept { return "ALike"; } int count() { return 42; } }; struct ALikeToo { - std::string_view name() const { return "ALikeToo"; } + std::string_view name() const noexcept { return "ALikeToo"; } int count() { return 99; } }; @@ -33,8 +32,8 @@ static void Direct_Call(benchmark::State& state) { BENCHMARK(Direct_Call); -static void Protocol_Virtual_Call(benchmark::State& state) { - xyz::protocol p(std::in_place_type); +static void Protocol_Call(benchmark::State& state) { + xyz::protocol p(std::in_place_type); benchmark::DoNotOptimize(p); for (auto _ : state) { benchmark::DoNotOptimize(p.name()); @@ -42,18 +41,7 @@ static void Protocol_Virtual_Call(benchmark::State& state) { } } -BENCHMARK(Protocol_Virtual_Call); - -static void Protocol_Manual_Call(benchmark::State& state) { - xyz::protocol p(std::in_place_type); - benchmark::DoNotOptimize(p); - for (auto _ : state) { - benchmark::DoNotOptimize(p.name()); - benchmark::DoNotOptimize(p.count()); - } -} - -BENCHMARK(Protocol_Manual_Call); +BENCHMARK(Protocol_Call); // Copy construction benchmarks static void Direct_Copy(benchmark::State& state) { @@ -66,25 +54,15 @@ static void Direct_Copy(benchmark::State& state) { BENCHMARK(Direct_Copy); -static void Protocol_Virtual_Copy(benchmark::State& state) { - xyz::protocol p(std::in_place_type); +static void Protocol_Copy(benchmark::State& state) { + xyz::protocol p(std::in_place_type); for (auto _ : state) { - xyz::protocol copy(p); + xyz::protocol copy(p); benchmark::DoNotOptimize(copy); } } -BENCHMARK(Protocol_Virtual_Copy); - -static void Protocol_Manual_Copy(benchmark::State& state) { - xyz::protocol p(std::in_place_type); - for (auto _ : state) { - xyz::protocol copy(p); - benchmark::DoNotOptimize(copy); - } -} - -BENCHMARK(Protocol_Manual_Copy); +BENCHMARK(Protocol_Copy); // Move construction/assignment benchmarks static void Direct_Move(benchmark::State& state) { @@ -99,29 +77,17 @@ static void Direct_Move(benchmark::State& state) { BENCHMARK(Direct_Move); -static void Protocol_Virtual_Move(benchmark::State& state) { - xyz::protocol p(std::in_place_type); - for (auto _ : state) { - xyz::protocol moved(std::move(p)); - benchmark::DoNotOptimize(moved); - p = std::move(moved); - benchmark::DoNotOptimize(p); - } -} - -BENCHMARK(Protocol_Virtual_Move); - -static void Protocol_Manual_Move(benchmark::State& state) { - xyz::protocol p(std::in_place_type); +static void Protocol_Move(benchmark::State& state) { + xyz::protocol p(std::in_place_type); for (auto _ : state) { - xyz::protocol moved(std::move(p)); + xyz::protocol moved(std::move(p)); benchmark::DoNotOptimize(moved); p = std::move(moved); benchmark::DoNotOptimize(p); } } -BENCHMARK(Protocol_Manual_Move); +BENCHMARK(Protocol_Move); // Swap benchmarks static void Direct_Swap(benchmark::State& state) { @@ -136,21 +102,9 @@ static void Direct_Swap(benchmark::State& state) { BENCHMARK(Direct_Swap); -static void Protocol_Virtual_Swap(benchmark::State& state) { - xyz::protocol p1(std::in_place_type); - xyz::protocol p2(std::in_place_type); - for (auto _ : state) { - p1.swap(p2); - benchmark::DoNotOptimize(p1); - benchmark::DoNotOptimize(p2); - } -} - -BENCHMARK(Protocol_Virtual_Swap); - -static void Protocol_Manual_Swap(benchmark::State& state) { - xyz::protocol p1(std::in_place_type); - xyz::protocol p2(std::in_place_type); +static void Protocol_Swap(benchmark::State& state) { + xyz::protocol p1(std::in_place_type); + xyz::protocol p2(std::in_place_type); for (auto _ : state) { p1.swap(p2); benchmark::DoNotOptimize(p1); @@ -158,7 +112,7 @@ static void Protocol_Manual_Swap(benchmark::State& state) { } } -BENCHMARK(Protocol_Manual_Swap); +BENCHMARK(Protocol_Swap); // Construction and Destruction benchmarks static void Direct_CtorDtor(benchmark::State& state) { @@ -170,40 +124,19 @@ static void Direct_CtorDtor(benchmark::State& state) { BENCHMARK(Direct_CtorDtor); -static void Protocol_Virtual_CtorDtor(benchmark::State& state) { +static void Protocol_CtorDtor(benchmark::State& state) { for (auto _ : state) { - xyz::protocol p(std::in_place_type); + xyz::protocol p(std::in_place_type); benchmark::DoNotOptimize(p); } } -BENCHMARK(Protocol_Virtual_CtorDtor); - -static void Protocol_Manual_CtorDtor(benchmark::State& state) { - for (auto _ : state) { - xyz::protocol p(std::in_place_type); - benchmark::DoNotOptimize(p); - } -} - -BENCHMARK(Protocol_Manual_CtorDtor); +BENCHMARK(Protocol_CtorDtor); // View benchmarks -static void ProtocolView_Virtual_Call(benchmark::State& state) { - ALike alike; - xyz::protocol_view view(alike); - benchmark::DoNotOptimize(view); - for (auto _ : state) { - benchmark::DoNotOptimize(view.name()); - benchmark::DoNotOptimize(view.count()); - } -} - -BENCHMARK(ProtocolView_Virtual_Call); - -static void ProtocolView_Manual_Call(benchmark::State& state) { +static void ProtocolView_Call(benchmark::State& state) { ALike alike; - xyz::protocol_view view(alike); + xyz::protocol_view view(alike); benchmark::DoNotOptimize(view); for (auto _ : state) { benchmark::DoNotOptimize(view.name()); @@ -211,7 +144,7 @@ static void ProtocolView_Manual_Call(benchmark::State& state) { } } -BENCHMARK(ProtocolView_Manual_Call); +BENCHMARK(ProtocolView_Call); static void RawPointer_Call(benchmark::State& state) { ALike alike; @@ -226,32 +159,11 @@ static void RawPointer_Call(benchmark::State& state) { BENCHMARK(RawPointer_Call); // Jitter benchmarks to defeat branch prediction -static void ProtocolView_Virtual_Call_Jitter(benchmark::State& state) { - ALike a1; - ALikeToo a2; - xyz::protocol_view views[2] = { - xyz::protocol_view(a1), - xyz::protocol_view(a2)}; - - benchmark::DoNotOptimize(views); - - size_t i = 0; - for (auto _ : state) { - auto& view = views[i & 1]; - benchmark::DoNotOptimize(view.name()); - benchmark::DoNotOptimize(view.count()); - ++i; - } -} - -BENCHMARK(ProtocolView_Virtual_Call_Jitter); - -static void ProtocolView_Manual_Call_Jitter(benchmark::State& state) { +static void ProtocolView_Call_Jitter(benchmark::State& state) { ALike a1; ALikeToo a2; - xyz::protocol_view views[2] = { - xyz::protocol_view(a1), - xyz::protocol_view(a2)}; + xyz::protocol_view views[2] = {xyz::protocol_view(a1), + xyz::protocol_view(a2)}; benchmark::DoNotOptimize(views); @@ -264,7 +176,7 @@ static void ProtocolView_Manual_Call_Jitter(benchmark::State& state) { } } -BENCHMARK(ProtocolView_Manual_Call_Jitter); +BENCHMARK(ProtocolView_Call_Jitter); static void RawPointer_Call_Jitter(benchmark::State& state) { ALike a1; diff --git a/scripts/cmake.py b/scripts/cmake.py index f166b6b..ecfca13 100644 --- a/scripts/cmake.py +++ b/scripts/cmake.py @@ -31,11 +31,6 @@ def main() -> None: const="Release", help="Use Release preset (default)", ) - parser.add_argument( - "--manual-vtable", - action="store_true", - help="Set XYZ_PROTOCOL_GENERATE_MANUAL_VTABLE=ON", - ) parser.add_argument("--asan", action="store_true", help="Enable Address Sanitizer") parser.add_argument( "--ubsan", action="store_true", help="Enable Undefined Behaviour Sanitizer" @@ -70,13 +65,11 @@ def log(msg: Any) -> None: if args.verbose: print(msg) - manual_vtable_val = "ON" if args.manual_vtable else "OFF" # Configure step configure_args = [ "cmake", "--preset", preset, - f"-DXYZ_PROTOCOL_GENERATE_MANUAL_VTABLE={manual_vtable_val}", f"-DENABLE_ASAN={'ON' if args.asan else 'OFF'}", f"-DENABLE_UBSAN={'ON' if args.ubsan else 'OFF'}", f"-DENABLE_TSAN={'ON' if args.tsan else 'OFF'}", diff --git a/scripts/protocol.j2 b/scripts/protocol.j2 index 0361400..55189ee 100644 --- a/scripts/protocol.j2 +++ b/scripts/protocol.j2 @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -52,36 +51,23 @@ concept protocol_concept_{{ c.name }} = protocol_const_concept_{{ c.name }}{% {% endfor %} }{% endif %}; -class protocol_view_const_cb_{{ c.name }} { - public: - constexpr virtual ~protocol_view_const_cb_{{ c.name }}() = default; +struct const_view_vtable_{{ c.name }} { {% for m in c.methods %}{% if m.is_const %} {% set params = [] %} - {% for a in m.arguments %}{% set _ = params.append(a.type.name ~ " a" ~ loop.index0) %}{% endfor %} + {% for a in m.arguments %}{% set _ = params.append(a.type.name) %}{% endfor %} {% set params_str = params | join(", ") %} - virtual {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(const void* ptr{% if params %}, {% endif %}{{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} = 0; + {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(const void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; {% endif %}{% endfor %} }; -class protocol_view_cb_{{ c.name }} : public protocol_view_const_cb_{{ c.name }} { - public: -{% for m in c.methods %}{% if m.is_const %} - using protocol_view_const_cb_{{ c.name }}::{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; -{% endif %}{% endfor %} -{% for m in c.methods %}{% if not m.is_const %} - {% set params = [] %} - {% for a in m.arguments %}{% set _ = params.append(a.type.name ~ " a" ~ loop.index0) %}{% endfor %} - {% set params_str = params | join(", ") %} - virtual {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(void* ptr{% if params %}, {% endif %}{{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} = 0; -{% endif %}{% endfor %} -}; +{% set const_methods = [] %} +{% set const_method_indices = [] %} +{% for m in c.methods %}{% if m.is_const %}{% set _ = const_methods.append(m) %}{% set _ = const_method_indices.append(loop.index0) %}{% endif %}{% endfor %} template -class protocol_view_dcb_{{ c.name }} : public protocol_view_cb_{{ c.name }} { - public: - constexpr protocol_view_dcb_{{ c.name }}() = default; - constexpr ~protocol_view_dcb_{{ c.name }}() override = default; -{% for m in c.methods %} +inline constexpr const_view_vtable_{{ c.name }} const_view_vtable_{{ c.name }}_for = { +{% for m in const_methods %} + {% set i = const_method_indices[loop.index0] %} {% set params = [] %} {% set passes = [] %} {% for a in m.arguments %} @@ -90,27 +76,31 @@ class protocol_view_dcb_{{ c.name }} : public protocol_view_cb_{{ c.name }} { {% endfor %} {% set params_str = params | join(", ") %} {% set passes_str = passes | join(", ") %} - {% if m.is_const %} - {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(const void* ptr{% if params %}, {% endif %}{{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} override { + [](const void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} -> {{ m.return_type.name }} { {% if m.return_type.name != 'void' %}return {% endif %}static_cast(ptr)->{{ m.name }}({{ passes_str }}); - } - {% else %} - {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(void* ptr{% if params %}, {% endif %}{{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} override { - {% if m.return_type.name != 'void' %}return {% endif %}static_cast(ptr)->{{ m.name }}({{ passes_str }}); - } - {% endif %} + }{% if not loop.last %},{% endif %} {% endfor %} }; -template -inline constexpr protocol_view_dcb_{{ c.name }} protocol_view_dcb_{{ c.name }}_inst{}; +struct view_vtable_{{ c.name }} { + const_view_vtable_{{ c.name }} const_view; +{% for m in c.methods %}{% if not m.is_const %} + {% set params = [] %} + {% for a in m.arguments %}{% set _ = params.append(a.type.name) %}{% endfor %} + {% set params_str = params | join(", ") %} + {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; +{% endif %}{% endfor %} +}; + +{% set non_const_methods = [] %} +{% set non_const_method_indices = [] %} +{% for m in c.methods %}{% if not m.is_const %}{% set _ = non_const_methods.append(m) %}{% set _ = non_const_method_indices.append(loop.index0) %}{% endif %}{% endfor %} template -class protocol_view_const_dcb_{{ c.name }} final : public protocol_view_const_cb_{{ c.name }} { - public: - constexpr protocol_view_const_dcb_{{ c.name }}() = default; - constexpr ~protocol_view_const_dcb_{{ c.name }}() override = default; -{% for m in c.methods %}{% if m.is_const %} +inline constexpr view_vtable_{{ c.name }} view_vtable_{{ c.name }}_for = { + const_view_vtable_{{ c.name }}_for{% if non_const_methods %},{% endif %} +{% for m in non_const_methods %} + {% set i = non_const_method_indices[loop.index0] %} {% set params = [] %} {% set passes = [] %} {% for a in m.arguments %} @@ -119,108 +109,71 @@ class protocol_view_const_dcb_{{ c.name }} final : public protocol_view_const_cb {% endfor %} {% set params_str = params | join(", ") %} {% set passes_str = passes | join(", ") %} - {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(const void* ptr{% if params %}, {% endif %}{{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} override { - {% if m.return_type.name != 'void' %}return {% endif %}static_cast(ptr)->{{ m.name }}({{ passes_str }}); - } -{% endif %}{% endfor %} + [](void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} -> {{ m.return_type.name }} { + {% if m.return_type.name != 'void' %}return {% endif %}static_cast(ptr)->{{ m.name }}({{ passes_str }}); + }{% if not loop.last %},{% endif %} +{% endfor %} }; -template -inline constexpr protocol_view_const_dcb_{{ c.name }} protocol_view_const_dcb_{{ c.name }}_inst{}; - template class protocol<{{ full_class_name }}, Allocator> { friend class protocol_view<{{ full_class_name }}>; friend class protocol_view; - class control_block { - public: - virtual control_block* xyz_protocol_clone(const Allocator& alloc) = 0; - virtual control_block* xyz_protocol_move(const Allocator& alloc) = 0; - virtual void xyz_protocol_destroy(const Allocator& alloc) = 0; - virtual const void* xyz_protocol_get_ptr() const = 0; - virtual void* xyz_protocol_get_mutable_ptr() = 0; - virtual const protocol_view_cb_{{ c.name }}* xyz_protocol_as_view_cb() const = 0; - - public: + struct vtable { + void* (*xyz_protocol_clone)(void* cb, const Allocator& alloc); + void* (*xyz_protocol_move)(void* cb, const Allocator& alloc); + void (*xyz_protocol_destroy)(void* cb, const Allocator& alloc); + const view_vtable_{{ c.name }}* view_vt; {% for m in c.methods %} {% set params = [] %} {% for a in m.arguments %} - {% set _ = params.append(a.type.name ~ " a" ~ loop.index0) %} + {% set _ = params.append(a.type.name) %} {% endfor %} {% set params_str = params | join(", ") %} - virtual {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} = 0; + {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; {% endfor %} }; template - class direct_control_block final : public control_block, - public protocol_view_dcb_{{ c.name }} { - union uninitialized_storage { - T t_; - constexpr uninitialized_storage() {} - constexpr ~uninitialized_storage() {} - } storage_; - - using cb_allocator = typename std::allocator_traits< - Allocator>::template rebind_alloc>; - using cb_alloc_traits = std::allocator_traits; - - public: -{% for m in c.methods %} - using protocol_view_dcb_{{ c.name }}::{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; -{% endfor %} - template - constexpr direct_control_block(const Allocator& alloc, Ts&&... ts) { - cb_allocator cb_alloc(alloc); - cb_alloc_traits::construct(cb_alloc, std::addressof(storage_.t_), - std::forward(ts)...); - } - - control_block* xyz_protocol_clone(const Allocator& alloc) override { - cb_allocator cb_alloc(alloc); - auto mem = cb_alloc_traits::allocate(cb_alloc, 1); + struct vtable_impl { + using t_allocator = typename std::allocator_traits< + Allocator>::template rebind_alloc; + using t_alloc_traits = std::allocator_traits; + + static void* xyz_protocol_clone(void* cb, const Allocator& alloc) { + auto* self = static_cast(cb); + t_allocator t_alloc(alloc); + auto mem = t_alloc_traits::allocate(t_alloc, 1); try { - cb_alloc_traits::construct(cb_alloc, mem, alloc, storage_.t_); + t_alloc_traits::construct(t_alloc, mem, *self); return mem; } catch (...) { - cb_alloc_traits::deallocate(cb_alloc, mem, 1); + t_alloc_traits::deallocate(t_alloc, mem, 1); throw; } } - control_block* xyz_protocol_move(const Allocator& alloc) override { - cb_allocator cb_alloc(alloc); - auto mem = cb_alloc_traits::allocate(cb_alloc, 1); + static void* xyz_protocol_move(void* cb, const Allocator& alloc) { + auto* self = static_cast(cb); + t_allocator t_alloc(alloc); + auto mem = t_alloc_traits::allocate(t_alloc, 1); try { - cb_alloc_traits::construct(cb_alloc, mem, alloc, - std::move(storage_.t_)); + t_alloc_traits::construct(t_alloc, mem, std::move(*self)); return mem; } catch (...) { - cb_alloc_traits::deallocate(cb_alloc, mem, 1); + t_alloc_traits::deallocate(t_alloc, mem, 1); throw; } } - void xyz_protocol_destroy(const Allocator& alloc) override { - cb_allocator cb_alloc(alloc); - cb_alloc_traits::destroy(cb_alloc, std::addressof(storage_.t_)); - cb_alloc_traits::deallocate(cb_alloc, this, 1); + static void xyz_protocol_destroy(void* cb, const Allocator& alloc) { + auto* self = static_cast(cb); + t_allocator t_alloc(alloc); + t_alloc_traits::destroy(t_alloc, self); + t_alloc_traits::deallocate(t_alloc, self, 1); } - const void* xyz_protocol_get_ptr() const override { - return std::addressof(storage_.t_); - } - - void* xyz_protocol_get_mutable_ptr() override { - return std::addressof(storage_.t_); - } - - const protocol_view_cb_{{ c.name }}* xyz_protocol_as_view_cb() const override { - return this; - } - - public: {% for m in c.methods %} {% set params = [] %} {% set passes = [] %} @@ -231,34 +184,51 @@ class protocol<{{ full_class_name }}, Allocator> { {% set params_str = params | join(", ") %} {% set passes_str = passes | join(", ") %} {% if m.return_type.name == 'void' %} - {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} override { storage_.t_.{{ m.name }}({{ passes_str }}); } + static {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} { + auto* self = static_cast(cb); + self->{{ m.name }}({{ passes_str }}); + } {% else %} - {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} override { return storage_.t_.{{ m.name }}({{ passes_str }}); } + static {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} { + auto* self = static_cast(cb); + return self->{{ m.name }}({{ passes_str }}); + } {% endif %} {% endfor %} + + static constexpr vtable vtable_ = { + xyz_protocol_clone, + xyz_protocol_move, + xyz_protocol_destroy, + &view_vtable_{{ c.name }}_for, +{% for m in c.methods %} + {{ m.name | mangle }}_{{ method_guids[loop.index0] }}{% if not loop.last %},{% endif %} +{% endfor %} + }; }; using allocator_traits = std::allocator_traits; template - [[nodiscard]] constexpr control_block* create_control_block( + [[nodiscard]] constexpr void* create_storage( Ts&&... ts) const { - using cb_allocator = typename std::allocator_traits< - Allocator>::template rebind_alloc>; - cb_allocator cb_alloc(alloc_); - using cb_alloc_traits = std::allocator_traits; - auto mem = cb_alloc_traits::allocate(cb_alloc, 1); + using t_allocator = typename std::allocator_traits< + Allocator>::template rebind_alloc; + t_allocator t_alloc(alloc_); + using t_alloc_traits = std::allocator_traits; + auto mem = t_alloc_traits::allocate(t_alloc, 1); try { - cb_alloc_traits::construct(cb_alloc, mem, alloc_, + t_alloc_traits::construct(t_alloc, mem, std::forward(ts)...); return mem; } catch (...) { - cb_alloc_traits::deallocate(cb_alloc, mem, 1); + t_alloc_traits::deallocate(t_alloc, mem, 1); throw; } } - control_block* cb_; + void* cb_; + const vtable* vtable_; [[no_unique_address]] Allocator alloc_; public: @@ -306,7 +276,8 @@ class protocol<{{ full_class_name }}, Allocator> { explicit constexpr protocol(std::allocator_arg_t, const Allocator& alloc) requires std::default_initializable<{{ full_class_name }}> && std::copy_constructible<{{ full_class_name }}> : alloc_(alloc) { - cb_ = create_control_block<{{ full_class_name }}>(); + cb_ = create_storage<{{ full_class_name }}>(); + vtable_ = &vtable_impl<{{ full_class_name }}>::vtable_; } template @@ -316,7 +287,8 @@ class protocol<{{ full_class_name }}, Allocator> { std::copy_constructible> && protocol_concept_{{ c.name }} : alloc_(alloc) { - cb_ = create_control_block>(std::forward(u)); + cb_ = create_storage>(std::forward(u)); + vtable_ = &vtable_impl>::vtable_; } template @@ -326,7 +298,8 @@ class protocol<{{ full_class_name }}, Allocator> { std::constructible_from && std::copy_constructible && protocol_concept_{{ c.name }} : alloc_(alloc) { - cb_ = create_control_block(std::forward(ts)...); + cb_ = create_storage(std::forward(ts)...); + vtable_ = &vtable_impl::vtable_; } template @@ -337,16 +310,19 @@ class protocol<{{ full_class_name }}, Allocator> { std::constructible_from, Ts&&...> && std::copy_constructible && protocol_concept_{{ c.name }} : alloc_(alloc) { - cb_ = create_control_block(ilist, std::forward(ts)...); + cb_ = create_storage(ilist, std::forward(ts)...); + vtable_ = &vtable_impl::vtable_; } constexpr protocol(std::allocator_arg_t, const Allocator& alloc, const protocol& other) : alloc_(alloc) { if (!other.valueless_after_move()) { - cb_ = other.cb_->xyz_protocol_clone(alloc_); + cb_ = other.vtable_->xyz_protocol_clone(other.cb_, alloc_); + vtable_ = other.vtable_; } else { cb_ = nullptr; + vtable_ = nullptr; } } @@ -356,14 +332,18 @@ class protocol<{{ full_class_name }}, Allocator> { : alloc_(alloc) { if constexpr (allocator_traits::is_always_equal::value) { cb_ = std::exchange(other.cb_, nullptr); + vtable_ = std::exchange(other.vtable_, nullptr); } else { if (alloc_ == other.alloc_) { cb_ = std::exchange(other.cb_, nullptr); + vtable_ = std::exchange(other.vtable_, nullptr); } else { if (!other.valueless_after_move()) { - cb_ = other.cb_->xyz_protocol_move(alloc_); + cb_ = other.vtable_->xyz_protocol_move(other.cb_, alloc_); + vtable_ = other.vtable_; } else { cb_ = nullptr; + vtable_ = nullptr; } } } @@ -375,13 +355,14 @@ class protocol<{{ full_class_name }}, Allocator> { ~protocol() { if (cb_ != nullptr) { - cb_->xyz_protocol_destroy(alloc_); + vtable_->xyz_protocol_destroy(cb_, alloc_); } } protocol& operator=(protocol other) noexcept( allocator_traits::is_always_equal::value) { std::swap(cb_, other.cb_); + std::swap(vtable_, other.vtable_); if constexpr (!allocator_traits::is_always_equal::value) { std::swap(alloc_, other.alloc_); } @@ -391,6 +372,7 @@ class protocol<{{ full_class_name }}, Allocator> { void swap(protocol& other) noexcept( allocator_traits::is_always_equal::value) { std::swap(cb_, other.cb_); + std::swap(vtable_, other.vtable_); if constexpr (!allocator_traits::is_always_equal::value) { std::swap(alloc_, other.alloc_); } @@ -411,7 +393,7 @@ class protocol<{{ full_class_name }}, Allocator> { {% endfor %} {% set params_str = params | join(", ") %} {% set passes_str = passes | join(", ") %} - {{ m.return_type.name }} {{ m.name }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} { return cb_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}({{ passes_str }}); } + {{ m.return_type.name }} {{ m.name }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} { return vtable_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(cb_{% if passes %}, {% endif %}{{ passes_str }}); } {% endfor %} }; @@ -421,17 +403,17 @@ class protocol_view { friend class protocol_view<{{ full_class_name }}>; const void* ptr_; - const protocol_view_const_cb_{{ c.name }}* cb_; + const const_view_vtable_{{ c.name }}* vptr_; constexpr protocol_view(const void* ptr, - const protocol_view_const_cb_{{ c.name }}* cb) noexcept - : ptr_(ptr), cb_(cb) {} + const const_view_vtable_{{ c.name }}* vptr) noexcept + : ptr_(ptr), vptr_(vptr) {} template static const void* checked_ptr( const protocol<{{ full_class_name }}, Alloc>& p) noexcept { assert(!p.valueless_after_move()); - return p.cb_->xyz_protocol_get_ptr(); + return p.cb_; } public: @@ -441,7 +423,7 @@ class protocol_view { (!std::same_as, protocol_view >) constexpr protocol_view(const T& obj) noexcept : ptr_(std::addressof(obj)), - cb_(&protocol_view_const_dcb_{{ c.name }}_inst>) {} + vptr_(&const_view_vtable_{{ c.name }}_for>) {} template requires protocol_const_concept_{{ c.name }} && @@ -452,7 +434,7 @@ class protocol_view { template protocol_view(const protocol<{{ full_class_name }}, Alloc>& p) noexcept : ptr_(checked_ptr(p)), - cb_(p.cb_->xyz_protocol_as_view_cb()) {} + vptr_(&p.vtable_->view_vt->const_view) {} template protocol_view(const protocol<{{ full_class_name }}, Alloc>&&) = delete; @@ -460,7 +442,7 @@ class protocol_view { template protocol_view(protocol<{{ full_class_name }}, Alloc>& p) noexcept : ptr_(checked_ptr(p)), - cb_(p.cb_->xyz_protocol_as_view_cb()) {} + vptr_(&p.vtable_->view_vt->const_view) {} {% for m in c.methods %}{% if m.is_const %} {% set params = [] %} @@ -472,7 +454,7 @@ class protocol_view { {% set params_str = params | join(", ") %} {% set passes_str = passes | join(", ") %} {{ m.return_type.name }} {{ m.name }}({{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} { - {% if m.return_type.name != 'void' %}return {% endif %}cb_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); + {% if m.return_type.name != 'void' %}return {% endif %}vptr_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); } {% endif %}{% endfor %} }; @@ -480,12 +462,12 @@ class protocol_view { template <> class protocol_view<{{ full_class_name }}> { void* ptr_; - const protocol_view_cb_{{ c.name }}* cb_; + const view_vtable_{{ c.name }}* vptr_; template static void* checked_ptr(protocol<{{ full_class_name }}, Alloc>& p) noexcept { assert(!p.valueless_after_move()); - return p.cb_->xyz_protocol_get_mutable_ptr(); + return p.cb_; } public: @@ -495,15 +477,15 @@ class protocol_view<{{ full_class_name }}> { (!std::same_as, protocol_view >) constexpr protocol_view(T& obj) noexcept : ptr_(std::addressof(obj)), - cb_(&protocol_view_dcb_{{ c.name }}_inst>) {} + vptr_(&view_vtable_{{ c.name }}_for>) {} template protocol_view(protocol<{{ full_class_name }}, Alloc>& p) noexcept : ptr_(checked_ptr(p)), - cb_(p.cb_->xyz_protocol_as_view_cb()) {} + vptr_(p.vtable_->view_vt) {} constexpr operator protocol_view() const noexcept { - return protocol_view{static_cast(ptr_), cb_}; + return protocol_view{static_cast(ptr_), &vptr_->const_view}; } {% for m in c.methods %} @@ -516,7 +498,11 @@ class protocol_view<{{ full_class_name }}> { {% set params_str = params | join(", ") %} {% set passes_str = passes | join(", ") %} {{ m.return_type.name }} {{ m.name }}({{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} { - {% if m.return_type.name != 'void' %}return {% endif %}cb_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); + {% if m.is_const %} + {% if m.return_type.name != 'void' %}return {% endif %}vptr_->const_view.{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); + {% else %} + {% if m.return_type.name != 'void' %}return {% endif %}vptr_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); + {% endif %} } {% endfor %} }; diff --git a/scripts/protocol_manual_vtable.j2 b/scripts/protocol_manual_vtable.j2 deleted file mode 100644 index 2f22eb8..0000000 --- a/scripts/protocol_manual_vtable.j2 +++ /dev/null @@ -1,510 +0,0 @@ -// BEGIN Generated code for protocol_{{ c.name }} -#include -#include -#include -#include -#include -#include -#include - -#include "protocol.h" -#include "{{ header }}" - -{% set full_class_name = "::" ~ c.namespace ~ "::" ~ c.name if c.namespace else c.name %} - -namespace xyz { - -template -concept protocol_const_concept_{{ c.name }} = requires(const T& t) { -{% for m in c.methods %} - {% if m.is_const %} - {% set declvals = [] %} - {% for a in m.arguments %} - {% set _ = declvals.append("std::declval<" ~ a.type.name ~ ">()") %} - {% endfor %} - {% set args_str = declvals | join(", ") %} - {% if m.return_type.name == 'void' %} - { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %}; - {% else %} - { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %} -> std::convertible_to<{{ m.return_type.name }}>; - {% endif %} - {% endif %} -{% endfor %} -}; - -{% set non_const_methods = [] %} -{% for m in c.methods %}{% if not m.is_const %}{% set _ = non_const_methods.append(m) %}{% endif %}{% endfor %} - -template -concept protocol_concept_{{ c.name }} = protocol_const_concept_{{ c.name }}{% if non_const_methods %} && requires(T& t) { -{% for m in non_const_methods %} - {% set declvals = [] %} - {% for a in m.arguments %} - {% set _ = declvals.append("std::declval<" ~ a.type.name ~ ">()") %} - {% endfor %} - {% set args_str = declvals | join(", ") %} - {% if m.return_type.name == 'void' %} - { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %}; - {% else %} - { t.{{ m.name }}({{ args_str }}) }{% if m.is_noexcept %} noexcept{% endif %} -> std::convertible_to<{{ m.return_type.name }}>; - {% endif %} -{% endfor %} -}{% endif %}; - -struct const_view_vtable_{{ c.name }} { -{% for m in c.methods %}{% if m.is_const %} - {% set params = [] %} - {% for a in m.arguments %}{% set _ = params.append(a.type.name) %}{% endfor %} - {% set params_str = params | join(", ") %} - {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(const void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; -{% endif %}{% endfor %} -}; - -{% set const_methods = [] %} -{% set const_method_indices = [] %} -{% for m in c.methods %}{% if m.is_const %}{% set _ = const_methods.append(m) %}{% set _ = const_method_indices.append(loop.index0) %}{% endif %}{% endfor %} - -template -inline constexpr const_view_vtable_{{ c.name }} const_view_vtable_{{ c.name }}_for = { -{% for m in const_methods %} - {% set i = const_method_indices[loop.index0] %} - {% set params = [] %} - {% set passes = [] %} - {% for a in m.arguments %} - {% set _ = params.append(a.type.name ~ " a" ~ loop.index0) %} - {% set _ = passes.append("std::forward(a" ~ loop.index0 ~ ")") %} - {% endfor %} - {% set params_str = params | join(", ") %} - {% set passes_str = passes | join(", ") %} - [](const void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} -> {{ m.return_type.name }} { - {% if m.return_type.name != 'void' %}return {% endif %}static_cast(ptr)->{{ m.name }}({{ passes_str }}); - }{% if not loop.last %},{% endif %} -{% endfor %} -}; - -struct view_vtable_{{ c.name }} { - const_view_vtable_{{ c.name }} const_view; -{% for m in c.methods %}{% if not m.is_const %} - {% set params = [] %} - {% for a in m.arguments %}{% set _ = params.append(a.type.name) %}{% endfor %} - {% set params_str = params | join(", ") %} - {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; -{% endif %}{% endfor %} -}; - -{% set non_const_methods = [] %} -{% set non_const_method_indices = [] %} -{% for m in c.methods %}{% if not m.is_const %}{% set _ = non_const_methods.append(m) %}{% set _ = non_const_method_indices.append(loop.index0) %}{% endif %}{% endfor %} - -template -inline constexpr view_vtable_{{ c.name }} view_vtable_{{ c.name }}_for = { - const_view_vtable_{{ c.name }}_for{% if non_const_methods %},{% endif %} -{% for m in non_const_methods %} - {% set i = non_const_method_indices[loop.index0] %} - {% set params = [] %} - {% set passes = [] %} - {% for a in m.arguments %} - {% set _ = params.append(a.type.name ~ " a" ~ loop.index0) %} - {% set _ = passes.append("std::forward(a" ~ loop.index0 ~ ")") %} - {% endfor %} - {% set params_str = params | join(", ") %} - {% set passes_str = passes | join(", ") %} - [](void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} -> {{ m.return_type.name }} { - {% if m.return_type.name != 'void' %}return {% endif %}static_cast(ptr)->{{ m.name }}({{ passes_str }}); - }{% if not loop.last %},{% endif %} -{% endfor %} -}; - -template -class protocol<{{ full_class_name }}, Allocator> { - friend class protocol_view<{{ full_class_name }}>; - friend class protocol_view; - - struct vtable { - void* (*xyz_protocol_clone)(void* cb, const Allocator& alloc); - void* (*xyz_protocol_move)(void* cb, const Allocator& alloc); - void (*xyz_protocol_destroy)(void* cb, const Allocator& alloc); - const view_vtable_{{ c.name }}* view_vt; -{% for m in c.methods %} - {% set params = [] %} - {% for a in m.arguments %} - {% set _ = params.append(a.type.name) %} - {% endfor %} - {% set params_str = params | join(", ") %} - {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; -{% endfor %} - }; - - template - struct vtable_impl { - using t_allocator = typename std::allocator_traits< - Allocator>::template rebind_alloc; - using t_alloc_traits = std::allocator_traits; - - static void* xyz_protocol_clone(void* cb, const Allocator& alloc) { - auto* self = static_cast(cb); - t_allocator t_alloc(alloc); - auto mem = t_alloc_traits::allocate(t_alloc, 1); - try { - t_alloc_traits::construct(t_alloc, mem, *self); - return mem; - } catch (...) { - t_alloc_traits::deallocate(t_alloc, mem, 1); - throw; - } - } - - static void* xyz_protocol_move(void* cb, const Allocator& alloc) { - auto* self = static_cast(cb); - t_allocator t_alloc(alloc); - auto mem = t_alloc_traits::allocate(t_alloc, 1); - try { - t_alloc_traits::construct(t_alloc, mem, std::move(*self)); - return mem; - } catch (...) { - t_alloc_traits::deallocate(t_alloc, mem, 1); - throw; - } - } - - static void xyz_protocol_destroy(void* cb, const Allocator& alloc) { - auto* self = static_cast(cb); - t_allocator t_alloc(alloc); - t_alloc_traits::destroy(t_alloc, self); - t_alloc_traits::deallocate(t_alloc, self, 1); - } - -{% for m in c.methods %} - {% set params = [] %} - {% set passes = [] %} - {% for a in m.arguments %} - {% set _ = params.append(a.type.name ~ " a" ~ loop.index0) %} - {% set _ = passes.append("std::forward(a" ~ loop.index0 ~ ")") %} - {% endfor %} - {% set params_str = params | join(", ") %} - {% set passes_str = passes | join(", ") %} - {% if m.return_type.name == 'void' %} - static {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} { - auto* self = static_cast(cb); - self->{{ m.name }}({{ passes_str }}); - } - {% else %} - static {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} { - auto* self = static_cast(cb); - return self->{{ m.name }}({{ passes_str }}); - } - {% endif %} -{% endfor %} - - static constexpr vtable vtable_ = { - xyz_protocol_clone, - xyz_protocol_move, - xyz_protocol_destroy, - &view_vtable_{{ c.name }}_for, -{% for m in c.methods %} - {{ m.name | mangle }}_{{ method_guids[loop.index0] }}{% if not loop.last %},{% endif %} -{% endfor %} - }; - }; - - using allocator_traits = std::allocator_traits; - - template - [[nodiscard]] constexpr void* create_storage( - Ts&&... ts) const { - using t_allocator = typename std::allocator_traits< - Allocator>::template rebind_alloc; - t_allocator t_alloc(alloc_); - using t_alloc_traits = std::allocator_traits; - auto mem = t_alloc_traits::allocate(t_alloc, 1); - try { - t_alloc_traits::construct(t_alloc, mem, std::forward(ts)...); - return mem; - } catch (...) { - t_alloc_traits::deallocate(t_alloc, mem, 1); - throw; - } - } - - void* cb_; - const vtable* vtable_; - [[no_unique_address]] Allocator alloc_; - - public: - explicit constexpr protocol() - requires std::default_initializable<{{ full_class_name }}> && protocol_concept_{{ c.name }}<{{ full_class_name }}> && - std::copy_constructible<{{ full_class_name }}> - : protocol(std::allocator_arg_t{}, Allocator{}) {} - - template - constexpr explicit protocol(U&& u) - requires(!std::same_as>) && - std::copy_constructible> && - protocol_concept_{{ c.name }} - : protocol(std::allocator_arg_t{}, Allocator{}, std::forward(u)) {} - - template - explicit constexpr protocol(std::in_place_type_t, Ts&&... ts) - requires std::same_as, U> && - std::constructible_from && - std::copy_constructible && - std::default_initializable && protocol_concept_{{ c.name }} - : protocol(std::allocator_arg_t{}, Allocator{}, std::in_place_type, - std::forward(ts)...) {} - - template - explicit constexpr protocol(std::in_place_type_t, - std::initializer_list ilist, Ts&&... ts) - requires std::same_as, U> && - std::constructible_from, Ts&&...> && - std::copy_constructible && - std::default_initializable && protocol_concept_{{ c.name }} - : protocol(std::allocator_arg_t{}, Allocator{}, std::in_place_type, - ilist, std::forward(ts)...) {} - - constexpr protocol(const protocol& other) - : protocol(std::allocator_arg_t{}, - allocator_traits::select_on_container_copy_construction( - other.alloc_), - other) {} - - constexpr protocol(protocol&& other) noexcept( - allocator_traits::is_always_equal::value) - : protocol(std::allocator_arg_t{}, other.alloc_, std::move(other)) {} - - explicit constexpr protocol(std::allocator_arg_t, const Allocator& alloc) - requires std::default_initializable<{{ full_class_name }}> && std::copy_constructible<{{ full_class_name }}> - : alloc_(alloc) { - cb_ = create_storage<{{ full_class_name }}>(); - vtable_ = &vtable_impl<{{ full_class_name }}>::vtable_; - } - - template - constexpr explicit protocol(std::allocator_arg_t, const Allocator& alloc, - U&& u) - requires(not std::same_as>) && - std::copy_constructible> && - protocol_concept_{{ c.name }} - : alloc_(alloc) { - cb_ = create_storage>(std::forward(u)); - vtable_ = &vtable_impl>::vtable_; - } - - template - explicit constexpr protocol(std::allocator_arg_t, const Allocator& alloc, - std::in_place_type_t, Ts&&... ts) - requires std::same_as, U> && - std::constructible_from && - std::copy_constructible && protocol_concept_{{ c.name }} - : alloc_(alloc) { - cb_ = create_storage(std::forward(ts)...); - vtable_ = &vtable_impl::vtable_; - } - - template - explicit constexpr protocol(std::allocator_arg_t, const Allocator& alloc, - std::in_place_type_t, - std::initializer_list ilist, Ts&&... ts) - requires std::same_as, U> && - std::constructible_from, Ts&&...> && - std::copy_constructible && protocol_concept_{{ c.name }} - : alloc_(alloc) { - cb_ = create_storage(ilist, std::forward(ts)...); - vtable_ = &vtable_impl::vtable_; - } - - constexpr protocol(std::allocator_arg_t, const Allocator& alloc, - const protocol& other) - : alloc_(alloc) { - if (!other.valueless_after_move()) { - cb_ = other.vtable_->xyz_protocol_clone(other.cb_, alloc_); - vtable_ = other.vtable_; - } else { - cb_ = nullptr; - vtable_ = nullptr; - } - } - - constexpr protocol( - std::allocator_arg_t, const Allocator& alloc, - protocol&& other) noexcept(allocator_traits::is_always_equal::value) - : alloc_(alloc) { - if constexpr (allocator_traits::is_always_equal::value) { - cb_ = std::exchange(other.cb_, nullptr); - vtable_ = std::exchange(other.vtable_, nullptr); - } else { - if (alloc_ == other.alloc_) { - cb_ = std::exchange(other.cb_, nullptr); - vtable_ = std::exchange(other.vtable_, nullptr); - } else { - if (!other.valueless_after_move()) { - cb_ = other.vtable_->xyz_protocol_move(other.cb_, alloc_); - vtable_ = other.vtable_; - } else { - cb_ = nullptr; - vtable_ = nullptr; - } - } - } - } - - constexpr bool valueless_after_move() const noexcept { - return cb_ == nullptr; - } - - ~protocol() { - if (cb_ != nullptr) { - vtable_->xyz_protocol_destroy(cb_, alloc_); - } - } - - protocol& operator=(protocol other) noexcept( - allocator_traits::is_always_equal::value) { - std::swap(cb_, other.cb_); - std::swap(vtable_, other.vtable_); - if constexpr (!allocator_traits::is_always_equal::value) { - std::swap(alloc_, other.alloc_); - } - return *this; - } - - void swap(protocol& other) noexcept( - allocator_traits::is_always_equal::value) { - std::swap(cb_, other.cb_); - std::swap(vtable_, other.vtable_); - if constexpr (!allocator_traits::is_always_equal::value) { - std::swap(alloc_, other.alloc_); - } - } - - friend void swap(protocol& lhs, protocol& rhs) noexcept( - allocator_traits::is_always_equal::value) { - lhs.swap(rhs); - } - - public: -{% for m in c.methods %} - {% set params = [] %} - {% set passes = [] %} - {% for a in m.arguments %} - {% set _ = params.append(a.type.name ~ " a" ~ loop.index0) %} - {% set _ = passes.append("std::forward(a" ~ loop.index0 ~ ")") %} - {% endfor %} - {% set params_str = params | join(", ") %} - {% set passes_str = passes | join(", ") %} - {{ m.return_type.name }} {{ m.name }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} { return vtable_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(cb_{% if passes %}, {% endif %}{{ passes_str }}); } -{% endfor %} - -}; - -template <> -class protocol_view { - friend class protocol_view<{{ full_class_name }}>; - - const void* ptr_; - const const_view_vtable_{{ c.name }}* vptr_; - - constexpr protocol_view(const void* ptr, - const const_view_vtable_{{ c.name }}* vptr) noexcept - : ptr_(ptr), vptr_(vptr) {} - - template - static const void* checked_ptr( - const protocol<{{ full_class_name }}, Alloc>& p) noexcept { - assert(!p.valueless_after_move()); - return p.cb_; - } - - public: - template - requires protocol_const_concept_{{ c.name }} && - (!std::same_as, protocol_view<{{ full_class_name }}> >) && - (!std::same_as, protocol_view >) - constexpr protocol_view(const T& obj) noexcept - : ptr_(std::addressof(obj)), - vptr_(&const_view_vtable_{{ c.name }}_for>) {} - - template - requires protocol_const_concept_{{ c.name }} && - (!std::same_as, protocol_view<{{ full_class_name }}> >) && - (!std::same_as, protocol_view >) - protocol_view(const T&&) = delete; - - template - protocol_view(const protocol<{{ full_class_name }}, Alloc>& p) noexcept - : ptr_(checked_ptr(p)), - vptr_(&p.vtable_->view_vt->const_view) {} - - template - protocol_view(const protocol<{{ full_class_name }}, Alloc>&&) = delete; - - template - protocol_view(protocol<{{ full_class_name }}, Alloc>& p) noexcept - : ptr_(checked_ptr(p)), - vptr_(&p.vtable_->view_vt->const_view) {} - -{% for m in c.methods %}{% if m.is_const %} - {% set params = [] %} - {% set passes = [] %} - {% for a in m.arguments %} - {% set _ = params.append(a.type.name ~ " a" ~ loop.index0) %} - {% set _ = passes.append("std::forward(a" ~ loop.index0 ~ ")") %} - {% endfor %} - {% set params_str = params | join(", ") %} - {% set passes_str = passes | join(", ") %} - {{ m.return_type.name }} {{ m.name }}({{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} { - {% if m.return_type.name != 'void' %}return {% endif %}vptr_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); - } -{% endif %}{% endfor %} -}; - -template <> -class protocol_view<{{ full_class_name }}> { - void* ptr_; - const view_vtable_{{ c.name }}* vptr_; - - template - static void* checked_ptr(protocol<{{ full_class_name }}, Alloc>& p) noexcept { - assert(!p.valueless_after_move()); - return p.cb_; - } - - public: - template - requires protocol_concept_{{ c.name }} && - (!std::same_as, protocol_view<{{ full_class_name }}> >) && - (!std::same_as, protocol_view >) - constexpr protocol_view(T& obj) noexcept - : ptr_(std::addressof(obj)), - vptr_(&view_vtable_{{ c.name }}_for>) {} - - template - protocol_view(protocol<{{ full_class_name }}, Alloc>& p) noexcept - : ptr_(checked_ptr(p)), - vptr_(p.vtable_->view_vt) {} - - constexpr operator protocol_view() const noexcept { - return protocol_view{static_cast(ptr_), &vptr_->const_view}; - } - -{% for m in c.methods %} - {% set params = [] %} - {% set passes = [] %} - {% for a in m.arguments %} - {% set _ = params.append(a.type.name ~ " a" ~ loop.index0) %} - {% set _ = passes.append("std::forward(a" ~ loop.index0 ~ ")") %} - {% endfor %} - {% set params_str = params | join(", ") %} - {% set passes_str = passes | join(", ") %} - {{ m.return_type.name }} {{ m.name }}({{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} { - {% if m.is_const %} - {% if m.return_type.name != 'void' %}return {% endif %}vptr_->const_view.{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); - {% else %} - {% if m.return_type.name != 'void' %}return {% endif %}vptr_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); - {% endif %} - } -{% endfor %} -}; - -} // namespace xyz -// END Generated code for protocol_{{ c.name }} diff --git a/scripts/test_generate_protocol.py b/scripts/test_generate_protocol.py index 079cc20..0036721 100644 --- a/scripts/test_generate_protocol.py +++ b/scripts/test_generate_protocol.py @@ -224,8 +224,8 @@ class Ops { assert any(n.startswith("__operator__plus_equal__") for n in all_method_names) -def test_manual_vtable_template(temp_dir: str, compiler: str) -> None: - """Test that the manual vtable template produces a valid vtable structure.""" +def test_vtable_structures(temp_dir: str, compiler: str) -> None: + """Test that the generated code produces manual vtable structures.""" input_header = os.path.join(temp_dir, "input.h") output_header = os.path.join(temp_dir, "output.h") @@ -244,12 +244,12 @@ class Simple { output_header, "Simple", "input.h", - template_path="scripts/protocol_manual_vtable.j2", + template_path="scripts/protocol.j2", compiler=compiler, ) assert res.returncode == 0, res.stderr - # In manual vtable mode, we expect structs for vtables + # We expect structs for manual vtables with open(output_header, "r") as f: content = f.read() assert "struct const_view_vtable_Simple" in content From 80546e76f48024aa3d08c8fd401efa046e4c7089 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 13:06:22 +0100 Subject: [PATCH 02/11] Fix CI scripts --- .github/workflows/clang-tidy.yml | 15 ++++----------- .github/workflows/cmake.yml | 6 ++---- .github/workflows/iwyu.yml | 6 ++---- .github/workflows/sanitizers.yml | 14 ++------------ 4 files changed, 10 insertions(+), 31 deletions(-) diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index c83e25f..a4486e7 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -65,18 +65,11 @@ jobs: - uses: reviewdog/action-setup@v1.5.0 with: reviewdog_version: latest # Optional. [latest,nightly,v.X.Y.Z] - - name: Run CMake (Default VTable) + - name: Run CMake run: | - ./scripts/cmake.sh -B build_default --debug -DCLANG_TIDY_ENABLE=1 2>&1 | tee build1.log - - name: Run reviewdog (Default VTable) + ./scripts/cmake.sh --debug -DCLANG_TIDY_ENABLE=1 2>&1 | tee build.log + - name: Run reviewdog env: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - cat build1.log | reviewdog -reporter=github-pr-review -efm="%W%f:%l:%c: warning: %m" -efm="%E%f:%l:%c: error: %m" - - name: Run CMake (Manual VTable) - run: ./scripts/cmake.sh -B build_manual --manual-vtable --debug -DCLANG_TIDY_ENABLE=1 2>&1 | tee build2.log - - name: Run reviewdog (Manual VTable) - env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - cat build2.log | reviewdog -reporter=github-pr-review -efm="%W%f:%l:%c: warning: %m" -efm="%E%f:%l:%c: error: %m" + cat build.log | reviewdog -reporter=github-pr-review -efm="%W%f:%l:%c: warning: %m" -efm="%E%f:%l:%c: error: %m" diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index a1c2eb3..bcad3fd 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -235,7 +235,5 @@ jobs: uses: astral-sh/setup-uv@v5 - name: Set up Python run: uv sync - - name: Run CMake (Default VTable) - run: ./scripts/cmake.sh -B build_default --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }} - - name: Run CMake (Manual VTable) - run: ./scripts/cmake.sh -B build_manual --manual-vtable --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }} + - name: Run CMake + run: ./scripts/cmake.sh --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }} diff --git a/.github/workflows/iwyu.yml b/.github/workflows/iwyu.yml index 48da296..6cc19f2 100644 --- a/.github/workflows/iwyu.yml +++ b/.github/workflows/iwyu.yml @@ -71,7 +71,5 @@ jobs: python-version: "3.12" - name: Set up Python run: uv sync - - name: Run CMake (Default VTable) - run: ./scripts/cmake.sh -B build_default --debug -DIWYU_ENABLE=1 - - name: Run CMake (Manual VTable) - run: ./scripts/cmake.sh -B build_manual --manual-vtable --debug -DIWYU_ENABLE=1 + - name: Run CMake + run: ./scripts/cmake.sh --debug -DIWYU_ENABLE=1 diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index f5946af..2fffe37 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -48,23 +48,13 @@ jobs: uses: astral-sh/setup-uv@v5 - name: Set up Python run: uv sync - - name: Run CMake (Default VTable) + - name: Run CMake run: | UBSAN_FLAG="" if [ "${{ matrix.sanitizer }}" = "asan" ]; then UBSAN_FLAG="--ubsan" fi - ./scripts/cmake.sh -B build_${{ matrix.sanitizer }}_default --debug --${{ matrix.sanitizer }} ${UBSAN_FLAG} - env: - CC: clang-${{ matrix.clang_version }} - CXX: clang++-${{ matrix.clang_version }} - - name: Run CMake (Manual VTable) - run: | - UBSAN_FLAG="" - if [ "${{ matrix.sanitizer }}" = "asan" ]; then - UBSAN_FLAG="--ubsan" - fi - ./scripts/cmake.sh -B build_${{ matrix.sanitizer }}_manual --debug --manual-vtable --${{ matrix.sanitizer }} ${UBSAN_FLAG} + ./scripts/cmake.sh --debug --${{ matrix.sanitizer }} ${UBSAN_FLAG} env: CC: clang-${{ matrix.clang_version }} CXX: clang++-${{ matrix.clang_version }} From 04b6e86853c0f1f0522b89ed58fe5985715857a9 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 13:09:57 +0100 Subject: [PATCH 03/11] Fix python tests to expect manual vtable structures --- scripts/test_generate_protocol.py | 51 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/scripts/test_generate_protocol.py b/scripts/test_generate_protocol.py index 0036721..ac4cd2a 100644 --- a/scripts/test_generate_protocol.py +++ b/scripts/test_generate_protocol.py @@ -154,24 +154,24 @@ class Simple { model = get_model_from_file(output_header, compiler) - # Verify that the callback classes are generated + # Verify that the callback vtable structs are generated cb_classes = [c.name for c in model.classes] - assert "protocol_view_const_cb_Simple" in cb_classes - assert "protocol_view_cb_Simple" in cb_classes + assert "const_view_vtable_Simple" in cb_classes + assert "view_vtable_Simple" in cb_classes - # Check methods in protocol_view_cb_Simple and protocol_view_const_cb_Simple - cb_class = next(c for c in model.classes if c.name == "protocol_view_cb_Simple") + # Check members in view_vtable_Simple and const_view_vtable_Simple + cb_class = next(c for c in model.classes if c.name == "view_vtable_Simple") const_cb_class = next( - c for c in model.classes if c.name == "protocol_view_const_cb_Simple" + c for c in model.classes if c.name == "const_view_vtable_Simple" ) - all_method_names = [m.name for m in cb_class.methods] + [ - m.name for m in const_cb_class.methods + all_member_names = [m.name for m in cb_class.members] + [ + m.name for m in const_cb_class.members ] # Names should be mangled with GUIDs, so we check prefix - assert any(n.startswith("foo_") for n in all_method_names) - assert any(n.startswith("bar_") for n in all_method_names) + assert any(n.startswith("foo_") for n in all_member_names) + assert any(n.startswith("bar_") for n in all_member_names) def test_class_not_found(temp_dir: str, compiler: str) -> None: @@ -211,17 +211,14 @@ class Ops { assert res.returncode == 0, res.stderr model = get_model_from_file(output_header, compiler) - # The actual names are protocol_view_cb_Ops and protocol_view_const_cb_Ops - cb_class = next(c for c in model.classes if c.name == "protocol_view_cb_Ops") - const_cb_class = next( - c for c in model.classes if c.name == "protocol_view_const_cb_Ops" - ) + cb_class = next(c for c in model.classes if c.name == "view_vtable_Ops") + const_cb_class = next(c for c in model.classes if c.name == "const_view_vtable_Ops") - all_method_names = [m.name for m in cb_class.methods] + [ - m.name for m in const_cb_class.methods + all_member_names = [m.name for m in cb_class.members] + [ + m.name for m in const_cb_class.members ] - assert any(n.startswith("__operator__equal_equal__") for n in all_method_names) - assert any(n.startswith("__operator__plus_equal__") for n in all_method_names) + assert any(n.startswith("__operator__equal_equal__") for n in all_member_names) + assert any(n.startswith("__operator__plus_equal__") for n in all_member_names) def test_vtable_structures(temp_dir: str, compiler: str) -> None: @@ -370,21 +367,21 @@ class Overloaded { assert res.returncode == 0, res.stderr model = get_model_from_file(output_header, compiler) - cb_class = next(c for c in model.classes if c.name == "protocol_view_cb_Overloaded") + cb_class = next(c for c in model.classes if c.name == "view_vtable_Overloaded") const_cb_class = next( - c for c in model.classes if c.name == "protocol_view_const_cb_Overloaded" + c for c in model.classes if c.name == "const_view_vtable_Overloaded" ) - # Collect methods from both callback classes - all_foo_methods = [m for m in cb_class.methods if m.name.startswith("foo_")] + [ - m for m in const_cb_class.methods if m.name.startswith("foo_") + # Collect members from both vtable structs + all_foo_members = [m for m in cb_class.members if m.name.startswith("foo_")] + [ + m for m in const_cb_class.members if m.name.startswith("foo_") ] - # There should be exactly 3 methods starting with foo_ - assert len(all_foo_methods) == 3 + # There should be exactly 3 members starting with foo_ + assert len(all_foo_members) == 3 # Check that they have unique mangled names - mangled_names = [m.name for m in all_foo_methods] + mangled_names = [m.name for m in all_foo_members] assert len(set(mangled_names)) == 3 From 323f9fdb8835c47995bcdab266bae539b937d57f Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 13:20:47 +0100 Subject: [PATCH 04/11] CMake cleanup --- cmake/xyz_generate_protocol.cmake | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmake/xyz_generate_protocol.cmake b/cmake/xyz_generate_protocol.cmake index 539fbc8..617eb1f 100644 --- a/cmake/xyz_generate_protocol.cmake +++ b/cmake/xyz_generate_protocol.cmake @@ -38,10 +38,9 @@ a Jinja2 template and a Python generation script. #]=======================================================================] macro(xyz_generate_protocol) - set(options "") set(oneValueArgs CLASS_NAME INTERFACE OUTPUT HEADER) set(multiValueArgs "") - cmake_parse_arguments(XYZ_GENERATE "${options}" "${oneValueArgs}" + cmake_parse_arguments(XYZ_GENERATE "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) set(TEMPLATE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/scripts/protocol.j2) From 829497d91307a6d93bff2d469fc59da423db92d0 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 13:32:56 +0100 Subject: [PATCH 05/11] Rename protocol pointer cb_ to p_ and update test suites --- scripts/protocol.j2 | 38 +++++++++++++++---------------- scripts/test_generate_protocol.py | 34 ++++++++++++++------------- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/scripts/protocol.j2 b/scripts/protocol.j2 index 55189ee..6215f31 100644 --- a/scripts/protocol.j2 +++ b/scripts/protocol.j2 @@ -227,7 +227,7 @@ class protocol<{{ full_class_name }}, Allocator> { } } - void* cb_; + void* p_; const vtable* vtable_; [[no_unique_address]] Allocator alloc_; @@ -276,7 +276,7 @@ class protocol<{{ full_class_name }}, Allocator> { explicit constexpr protocol(std::allocator_arg_t, const Allocator& alloc) requires std::default_initializable<{{ full_class_name }}> && std::copy_constructible<{{ full_class_name }}> : alloc_(alloc) { - cb_ = create_storage<{{ full_class_name }}>(); + p_ = create_storage<{{ full_class_name }}>(); vtable_ = &vtable_impl<{{ full_class_name }}>::vtable_; } @@ -287,7 +287,7 @@ class protocol<{{ full_class_name }}, Allocator> { std::copy_constructible> && protocol_concept_{{ c.name }} : alloc_(alloc) { - cb_ = create_storage>(std::forward(u)); + p_ = create_storage>(std::forward(u)); vtable_ = &vtable_impl>::vtable_; } @@ -298,7 +298,7 @@ class protocol<{{ full_class_name }}, Allocator> { std::constructible_from && std::copy_constructible && protocol_concept_{{ c.name }} : alloc_(alloc) { - cb_ = create_storage(std::forward(ts)...); + p_ = create_storage(std::forward(ts)...); vtable_ = &vtable_impl::vtable_; } @@ -310,7 +310,7 @@ class protocol<{{ full_class_name }}, Allocator> { std::constructible_from, Ts&&...> && std::copy_constructible && protocol_concept_{{ c.name }} : alloc_(alloc) { - cb_ = create_storage(ilist, std::forward(ts)...); + p_ = create_storage(ilist, std::forward(ts)...); vtable_ = &vtable_impl::vtable_; } @@ -318,10 +318,10 @@ class protocol<{{ full_class_name }}, Allocator> { const protocol& other) : alloc_(alloc) { if (!other.valueless_after_move()) { - cb_ = other.vtable_->xyz_protocol_clone(other.cb_, alloc_); + p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); vtable_ = other.vtable_; } else { - cb_ = nullptr; + p_ = nullptr; vtable_ = nullptr; } } @@ -331,18 +331,18 @@ class protocol<{{ full_class_name }}, Allocator> { protocol&& other) noexcept(allocator_traits::is_always_equal::value) : alloc_(alloc) { if constexpr (allocator_traits::is_always_equal::value) { - cb_ = std::exchange(other.cb_, nullptr); + p_ = std::exchange(other.p_, nullptr); vtable_ = std::exchange(other.vtable_, nullptr); } else { if (alloc_ == other.alloc_) { - cb_ = std::exchange(other.cb_, nullptr); + p_ = std::exchange(other.p_, nullptr); vtable_ = std::exchange(other.vtable_, nullptr); } else { if (!other.valueless_after_move()) { - cb_ = other.vtable_->xyz_protocol_move(other.cb_, alloc_); + p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_); vtable_ = other.vtable_; } else { - cb_ = nullptr; + p_ = nullptr; vtable_ = nullptr; } } @@ -350,18 +350,18 @@ class protocol<{{ full_class_name }}, Allocator> { } constexpr bool valueless_after_move() const noexcept { - return cb_ == nullptr; + return p_ == nullptr; } ~protocol() { - if (cb_ != nullptr) { - vtable_->xyz_protocol_destroy(cb_, alloc_); + if (p_ != nullptr) { + vtable_->xyz_protocol_destroy(p_, alloc_); } } protocol& operator=(protocol other) noexcept( allocator_traits::is_always_equal::value) { - std::swap(cb_, other.cb_); + std::swap(p_, other.p_); std::swap(vtable_, other.vtable_); if constexpr (!allocator_traits::is_always_equal::value) { std::swap(alloc_, other.alloc_); @@ -371,7 +371,7 @@ class protocol<{{ full_class_name }}, Allocator> { void swap(protocol& other) noexcept( allocator_traits::is_always_equal::value) { - std::swap(cb_, other.cb_); + std::swap(p_, other.p_); std::swap(vtable_, other.vtable_); if constexpr (!allocator_traits::is_always_equal::value) { std::swap(alloc_, other.alloc_); @@ -393,7 +393,7 @@ class protocol<{{ full_class_name }}, Allocator> { {% endfor %} {% set params_str = params | join(", ") %} {% set passes_str = passes | join(", ") %} - {{ m.return_type.name }} {{ m.name }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} { return vtable_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(cb_{% if passes %}, {% endif %}{{ passes_str }}); } + {{ m.return_type.name }} {{ m.name }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} { return vtable_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(p_{% if passes %}, {% endif %}{{ passes_str }}); } {% endfor %} }; @@ -413,7 +413,7 @@ class protocol_view { static const void* checked_ptr( const protocol<{{ full_class_name }}, Alloc>& p) noexcept { assert(!p.valueless_after_move()); - return p.cb_; + return p.p_; } public: @@ -467,7 +467,7 @@ class protocol_view<{{ full_class_name }}> { template static void* checked_ptr(protocol<{{ full_class_name }}, Alloc>& p) noexcept { assert(!p.valueless_after_move()); - return p.cb_; + return p.p_; } public: diff --git a/scripts/test_generate_protocol.py b/scripts/test_generate_protocol.py index ac4cd2a..def5120 100644 --- a/scripts/test_generate_protocol.py +++ b/scripts/test_generate_protocol.py @@ -154,19 +154,19 @@ class Simple { model = get_model_from_file(output_header, compiler) - # Verify that the callback vtable structs are generated - cb_classes = [c.name for c in model.classes] - assert "const_view_vtable_Simple" in cb_classes - assert "view_vtable_Simple" in cb_classes + # Verify that the vtable structs are generated + vtable_classes = [c.name for c in model.classes] + assert "const_view_vtable_Simple" in vtable_classes + assert "view_vtable_Simple" in vtable_classes # Check members in view_vtable_Simple and const_view_vtable_Simple - cb_class = next(c for c in model.classes if c.name == "view_vtable_Simple") - const_cb_class = next( + vtable_class = next(c for c in model.classes if c.name == "view_vtable_Simple") + const_vtable_class = next( c for c in model.classes if c.name == "const_view_vtable_Simple" ) - all_member_names = [m.name for m in cb_class.members] + [ - m.name for m in const_cb_class.members + all_member_names = [m.name for m in vtable_class.members] + [ + m.name for m in const_vtable_class.members ] # Names should be mangled with GUIDs, so we check prefix @@ -211,11 +211,13 @@ class Ops { assert res.returncode == 0, res.stderr model = get_model_from_file(output_header, compiler) - cb_class = next(c for c in model.classes if c.name == "view_vtable_Ops") - const_cb_class = next(c for c in model.classes if c.name == "const_view_vtable_Ops") + vtable_class = next(c for c in model.classes if c.name == "view_vtable_Ops") + const_vtable_class = next( + c for c in model.classes if c.name == "const_view_vtable_Ops" + ) - all_member_names = [m.name for m in cb_class.members] + [ - m.name for m in const_cb_class.members + all_member_names = [m.name for m in vtable_class.members] + [ + m.name for m in const_vtable_class.members ] assert any(n.startswith("__operator__equal_equal__") for n in all_member_names) assert any(n.startswith("__operator__plus_equal__") for n in all_member_names) @@ -367,14 +369,14 @@ class Overloaded { assert res.returncode == 0, res.stderr model = get_model_from_file(output_header, compiler) - cb_class = next(c for c in model.classes if c.name == "view_vtable_Overloaded") - const_cb_class = next( + vtable_class = next(c for c in model.classes if c.name == "view_vtable_Overloaded") + const_vtable_class = next( c for c in model.classes if c.name == "const_view_vtable_Overloaded" ) # Collect members from both vtable structs - all_foo_members = [m for m in cb_class.members if m.name.startswith("foo_")] + [ - m for m in const_cb_class.members if m.name.startswith("foo_") + all_foo_members = [m for m in vtable_class.members if m.name.startswith("foo_")] + [ + m for m in const_vtable_class.members if m.name.startswith("foo_") ] # There should be exactly 3 members starting with foo_ From c6f46d1a8e901152de5f9cac68c3b651f675cb7c Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 17:42:00 +0100 Subject: [PATCH 06/11] Add zero-cost narrowing conversions using a thread-safe vtable mapping registry Implement zero-overhead structural subtype substitution (narrowing) between compatible protocol and protocol_view types. Add implementation notes. --- CMakeLists.txt | 10 +- DRAFT.md | 42 +++-- implementation-notes.md | 104 ++++++++++++ interface_A_Subset.h | 12 ++ protocol.cc | 55 ++++++- protocol.h | 97 ++++++++++- protocol_test.cc | 283 +++++++++++++++++++++++++++++++++ scripts/protocol.j2 | 132 ++++++++++++++- scripts/test_concept_errors.py | 74 +++++++++ 9 files changed, 789 insertions(+), 20 deletions(-) create mode 100644 implementation-notes.md create mode 100644 interface_A_Subset.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 96e12d8..ce30b66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,6 +102,10 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) CLASS_NAME A INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_A.h HEADER interface_A.h OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A.h) + xyz_generate_protocol( + CLASS_NAME A_Subset INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_A_Subset.h + HEADER interface_A_Subset.h + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A_Subset.h) xyz_generate_protocol( CLASS_NAME B INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_B.h HEADER interface_B.h @@ -118,6 +122,7 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) add_custom_target( generate_protocols DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A.h + ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A_Subset.h ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_B.h ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_C.h ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_D.h) @@ -127,10 +132,13 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) protocol_test LINK_LIBRARIES protocol + protocol_cc FILES protocol_test.cc interface_A.h ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A.h + interface_A_Subset.h + ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_A_Subset.h interface_B.h ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_B.h interface_C.h @@ -142,7 +150,7 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) add_executable(protocol_benchmark protocol_benchmark.cc) - target_link_libraries(protocol_benchmark PRIVATE protocol benchmark::benchmark) + target_link_libraries(protocol_benchmark PRIVATE protocol protocol_cc benchmark::benchmark) add_dependencies(protocol_benchmark generate_protocols) target_include_directories(protocol_benchmark PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/DRAFT.md b/DRAFT.md index 3131f0c..2ba155a 100644 --- a/DRAFT.md +++ b/DRAFT.md @@ -2,11 +2,11 @@ ISO/IEC JTC1 SC22 WG21 Programming Language C++ -P4148R1 +P4148R2 Working Group: Library Evolution, Library -Date: 2026-05-12 +Date: 2026-06-07 _Jonathan Coe \<\>_ @@ -43,6 +43,10 @@ and code injection and focuses solely on the design of the class templates ## History +### Changes in revision R2 + +- Support zero-cost conversion from a compatible `protocol` or `protocol_view` to a narrower target interface (subtype substitution). + ### Changes in revision R1 - Clarify special member function generation for `protocol` and `protocol_view`. @@ -204,6 +208,19 @@ class protocol> { constexpr protocol(std::allocator_arg_t, const Allocator& alloc, protocol&& other) noexcept; // conditionally-generated + // Converting move constructor from any compatible protocol. + template + constexpr protocol(protocol&& other) noexcept; + + // Converting copy constructor from any compatible protocol. + template + constexpr protocol(const protocol& other); + + // Allocator-extended converting copy constructor from any compatible protocol. + template + constexpr protocol(std::allocator_arg_t, const Allocator& alloc, + const protocol& other); + // Destructor. ~protocol(); @@ -237,6 +254,10 @@ class protocol_view { template protocol_view(protocol&&) = delete; + // Converting constructor from any compatible mutable protocol_view. + template + protocol_view(const protocol_view& other); + // structural-subtype (const and non-const) member functions. std::string func0(std::string_view) const noexcept; double func1(double) const; @@ -275,6 +296,14 @@ class protocol_view { // Constructor from a mutable protocol_view. constexpr protocol_view(protocol_view view) noexcept; + // Converting constructor from any compatible const protocol_view. + template + protocol_view(const protocol_view& other); + + // Converting constructor from any compatible mutable protocol_view. + template + protocol_view(const protocol_view& other); + // structural-subtype const member functions. std::string func0(std::string_view) const noexcept; double func1(double) const; @@ -399,13 +428,6 @@ memory budgets per interface. `protocol`, like `polymorphic` and `function`, doe not prescribe any layout constraints and leaves details like small-buffer-optimization to be determined by implementers. -#### Subtype Substitution - -A `proxy` can be implicitly converted to a -`proxy` when `RichFacade` explicitly includes `LeanFacade` via -`add_facade`. Because `protocol` interfaces are plain, independent structs with -no declared relationship, the same zero-overhead conversion is not available. - #### Ownership Erasure `protocol` is uniquely owning, `protocol_view` is non-owning. @@ -423,7 +445,7 @@ The table below summarises the main design choices side by side. | Interface definition | C++ struct | `facade_builder` + dispatch objects (explicit) | | Interaction syntax | `p.draw()` | `p->draw()` | | Layout constraints | Implementation defined | Encoded in the Facade type | -| Subtype substitution | Unsupported | Implicit via `add_facade` | +| Subtype substitution | Supported | Implicit via `add_facade` | | Ownership model | Explicit | Erased | ### Design Alternatives diff --git a/implementation-notes.md b/implementation-notes.md new file mode 100644 index 0000000..42d842e --- /dev/null +++ b/implementation-notes.md @@ -0,0 +1,104 @@ +# C++ Protocol Reference Implementation Notes + +This document details the design and implementation of the `protocol` and `protocol_view` types, focusing on code generation, virtual dispatch, narrowing conversions, and concurrent safety. + +--- + +## 1. Code Generation via Clang AST + +Specializations of `protocol` and `protocol_view` are generated from user-defined interface structures by [scripts/generate_protocol.py](file:///workspace/scripts/generate_protocol.py) using the Jinja2 template [scripts/protocol.j2](file:///workspace/scripts/protocol.j2). + +1. **AST Parsing:** + * Uses `libclang` Python bindings (`clang.cindex`) to parse the target header file. + * Traverses the AST to construct a model of the C++ class, identifying all public non-virtual, non-template member functions. + * Extracts function attributes including: name, constness, exception specifications (`noexcept`), return types, and parameter types. + +2. **Name Mangling and Symbol Stability:** + * To prevent symbol name collisions in the generated structs, member function pointers in the vtable must be uniquely identified. + * Generates a stable suffix by computing the MD5 hash of the function signature (e.g. `func2(int,int)`) and taking the first 8 characters: + $$\text{Suffix} = \text{MD5}(\text{signature})[0..7]$$ + * Example: `int func2(int)` generates member `func2_0087aeab`. + * Pointers to these members remain stable and deterministic across compiler versions and independent generation runs. + +--- + +## 2. Manual Vtables and Member Function Invocation + +The implementation avoids compiler-generated virtual tables (`vtable`/`vptr`) to enforce value semantics, control layout constraints, and avoid runtime inheritance. Instead, it uses custom C++ structures of function pointers. + +1. **Vtable Layout:** + * For each interface, the generator produces two vtable layouts: + * `const_view_vtable_`: Holds function pointers mapping const member functions. + * `view_vtable_`: Holds a nested `const_view_vtable_` member followed by function pointers for non-const member functions. + * Function pointer signatures take a type-erased pointer (`const void*` or `void*`) as the first argument, followed by the function parameters. + +2. **Vtable Specialization:** + * For a concrete type `T`, static constexpr instances `const_view_vtable_for` and `view_vtable_for` are initialized with lambdas that cast the type-erased pointer back to the concrete type: + ```cpp + [](const void* ptr, Args... args) -> Ret { + return static_cast(ptr)->member_function(args...); + } + ``` + +3. **Invocation Path:** + * `protocol_view` stores a type-erased pointer `ptr_` and a pointer to the generated vtable `vptr_`. + * Calling a member function performs a single indirection: + ```cpp + vptr_->member_function_mangled(ptr_, args...); + ``` + * Because vtable pointers point to statically allocated, immutable structs (`const_view_vtable_for`), this is identical to a standard virtual call cost but without class hierarchy coupling. + +--- + +## 3. Narrowing Conversions (Subtype Substitution) + +A `protocol` or `protocol_view` for interface `A` can be converted to one for interface `B` if `B` is a subset (subtype) of `A`. + +1. **Constructor Constraints:** + * We define type traits `is_protocol` and `is_protocol_view` along with the concept `not_protocol_or_view`. + * These traits prevent concrete constructors from matching view/protocol types during conversions, avoiding recursion or self-wrapping. + +2. **Converting Views:** + * Conversions are enabled via templated copy constructors constrained by the target vtable size and layout compatibility: + ```cpp + template + requires (!std::same_as) + constexpr protocol_view(const protocol_view& other) + : ptr_(other.ptr_), + vptr_(get_or_create_mutable_vtable(other.vptr_)) {} + ``` + +3. **Converting Owning Protocols:** + * Allocator-extended and standard converting constructors construct the target `protocol` from the source `protocol`. + * If the allocators are equal, the storage pointer `p_` is moved directly (`std::exchange`) and the target vtable is mapped. + * If the allocators are not equal, the source's `xyz_protocol_move` or `xyz_protocol_clone` function is called to construct the value in the target allocator's storage. + +--- + +## 4. Vtable Registry & Concurrency + +When narrowing from `Other` to `Target`, a new vtable matching `Target`'s layout must be built and populated with function pointers extracted from `Other`'s vtable. This mapping occurs dynamically inside a global type-erased registry. + +1. **Registry Signature:** + ```cpp + const void* get_or_create_vtable_erased( + const void* from_vptr, const void* type_id, std::size_t to_vtable_size, + void (*mapper)(const void* from, void* to)); + ``` + +2. **The Cache:** + * Caches mapped vtables in a static `std::unordered_map` keyed by `CacheKey{from_vptr, type_id}`. + * `type_id` is the address of a static template local `type_id_anchor`, ensuring target vtable/allocator uniqueness. + * Values are stored as `std::unique_ptr`. Because the map is node-allocated, returned pointers to elements remain stable. + +3. **Split-Lock Pattern:** + * To prevent recursive deadlocks when nested conversions occur (e.g. mapping an owning vtable requires mapping its nested mutable vtable on the same thread), the mutex is **not** held during mapping. + * **Sequence:** + 1. Lock mutex, lookup key. If found, return pointer, unlock. + 2. If miss, unlock mutex. + 3. Allocate target vtable buffer in thread-local storage. + 4. Invoke `mapper()` to populate the new vtable. + 5. Lock mutex, try to insert the buffer using `cache.emplace()`. + 6. If insertion succeeds, publish and return pointer. + 7. If insertion fails (another thread inserted the key during step 3-4), the local buffer is destroyed, and the already-cached pointer is returned. + * This guarantees that all threads always resolve to the identical vtable pointer for a given conversion key, eliminating data races and leaks under high contention. diff --git a/interface_A_Subset.h b/interface_A_Subset.h new file mode 100644 index 0000000..2d57617 --- /dev/null +++ b/interface_A_Subset.h @@ -0,0 +1,12 @@ +#ifndef XYZ_PROTOCOL_INTERFACE_A_SUBSET_H +#define XYZ_PROTOCOL_INTERFACE_A_SUBSET_H +#include + +namespace xyz { + +struct A_Subset { + std::string_view name() const noexcept; +}; + +} // namespace xyz +#endif // XYZ_PROTOCOL_INTERFACE_A_SUBSET_H diff --git a/protocol.cc b/protocol.cc index 714b9fa..49855e8 100644 --- a/protocol.cc +++ b/protocol.cc @@ -20,4 +20,57 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "protocol.h" -// This source file exists purely to ensure that the protocol header is valid. +#include +#include +#include +#include + +namespace xyz { +namespace { + +struct CacheKey { + const void* from_vptr; + const void* type_id; + + bool operator==(const CacheKey&) const = default; +}; + +struct CacheKeyHash { + std::size_t operator()(const CacheKey& key) const { + std::size_t h1 = std::hash{}(key.from_vptr); + std::size_t h2 = std::hash{}(key.type_id); + return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); + } +}; + +} // namespace + +const void* get_or_create_vtable_erased( + const void* from_vptr, const void* type_id, std::size_t to_vtable_size, + void (*mapper)(const void* from, void* to)) { + if (from_vptr == nullptr) { + return nullptr; + } + + static std::unordered_map, CacheKeyHash> + cache; + static std::mutex mutex; + + CacheKey key{from_vptr, type_id}; + { + std::lock_guard lock(mutex); + auto it = cache.find(key); + if (it != cache.end()) { + return it->second.get(); + } + } + + auto vtable_data = std::make_unique(to_vtable_size); + mapper(from_vptr, vtable_data.get()); + + std::lock_guard lock(mutex); + auto [inserted_it, inserted] = cache.emplace(key, std::move(vtable_data)); + return inserted_it->second.get(); +} + +} // namespace xyz diff --git a/protocol.h b/protocol.h index 17620a0..3944d56 100644 --- a/protocol.h +++ b/protocol.h @@ -17,13 +17,108 @@ 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. ==============================================================================*/ - #ifndef XYZ_PROTOCOL_H_ #define XYZ_PROTOCOL_H_ #include +#include +#include +#include namespace xyz { +template +struct is_protocol : std::false_type {}; + +template +class protocol; + +template +struct is_protocol> : std::true_type {}; + +template +struct is_protocol_view : std::false_type {}; + +template +class protocol_view; + +template +struct is_protocol_view> : std::true_type {}; + +template +concept not_protocol_or_view = !is_protocol>::value && + !is_protocol_view>::value; + +template +struct protocol_vtable_traits; + +const void* get_or_create_vtable_erased( + const void* from_vptr, const void* type_id, std::size_t to_vtable_size, + void (*mapper)(const void* from, void* to)); + +template +const typename protocol_vtable_traits::const_vtable* +get_or_create_vtable( + const typename protocol_vtable_traits::const_vtable* + from_vptr) { + using FromVtable = + typename protocol_vtable_traits::const_vtable; + using ToVtable = typename protocol_vtable_traits::const_vtable; + + static const char type_id_anchor = 0; + + auto mapper = [](const void* from, void* to) { + map_vtable_members(static_cast(from), + static_cast(to)); + }; + + return static_cast(get_or_create_vtable_erased( + from_vptr, &type_id_anchor, sizeof(ToVtable), mapper)); +} + +template +const typename protocol_vtable_traits::vtable* +get_or_create_mutable_vtable( + const typename protocol_vtable_traits::vtable* from_vptr) { + using FromVtable = typename protocol_vtable_traits::vtable; + using ToVtable = typename protocol_vtable_traits::vtable; + + static const char type_id_anchor = 0; + + auto mapper = [](const void* from, void* to) { + map_mutable_vtable_members(static_cast(from), + static_cast(to)); + }; + + return static_cast(get_or_create_vtable_erased( + from_vptr, &type_id_anchor, sizeof(ToVtable), mapper)); +} + +template +struct protocol_owning_vtable_traits; + +template +const typename protocol_owning_vtable_traits::vtable* +get_or_create_owning_vtable(const typename protocol_owning_vtable_traits< + FromProtocol, Allocator>::vtable* from_vptr) { + if (from_vptr == nullptr) { + return nullptr; + } + using FromVtable = + typename protocol_owning_vtable_traits::vtable; + using ToVtable = + typename protocol_owning_vtable_traits::vtable; + + static const char type_id_anchor = 0; + + auto mapper = [](const void* from, void* to) { + map_owning_vtable_members(static_cast(from), + static_cast(to)); + }; + + return static_cast(get_or_create_vtable_erased( + from_vptr, &type_id_anchor, sizeof(ToVtable), mapper)); +} + template > class protocol { static_assert( diff --git a/protocol_test.cc b/protocol_test.cc index b525096..3fd4ed9 100644 --- a/protocol_test.cc +++ b/protocol_test.cc @@ -22,12 +22,15 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include +#include #include #include +#include #include #include #include "generated/protocol_A.h" +#include "generated/protocol_A_Subset.h" #include "generated/protocol_B.h" #include "generated/protocol_C.h" #include "generated/protocol_D.h" @@ -893,4 +896,284 @@ TEST(ProtocolViewTest, ProtocolViewDOperators) { EXPECT_EQ(d[5], 10); } +TEST(ProtocolViewTest, NarrowingConversion) { + ALike a_obj; + xyz::protocol_view view_a(a_obj); + + // 1. Convert mutable view of A to const view of A_Subset + xyz::protocol_view const_view_subset = view_a; + EXPECT_EQ(const_view_subset.name(), "ALike"); + + // 2. Convert mutable view of A to mutable view of A_Subset + xyz::protocol_view view_subset = view_a; + EXPECT_EQ(view_subset.name(), "ALike"); + + // 3. Convert const view of A to const view of A_Subset + xyz::protocol_view const_view_a(a_obj); + xyz::protocol_view const_view_subset2 = const_view_a; + EXPECT_EQ(const_view_subset2.name(), "ALike"); +} + +TEST(ProtocolTest, NarrowingMoveConversionEqualAllocators) { + xyz::protocol> p(std::in_place_type, + 42); + EXPECT_EQ(p.count(), 42); + + // Convert owning protocol using move constructor + xyz::protocol> p_subset = + std::move(p); + EXPECT_TRUE(p.valueless_after_move()); + EXPECT_FALSE(p_subset.valueless_after_move()); + EXPECT_EQ(p_subset.name(), "ALike"); +} + +TEST(ProtocolTest, NarrowingCopyConversion) { + xyz::protocol> p(std::in_place_type, + 42); + EXPECT_EQ(p.count(), 42); + + // Convert owning protocol using copy constructor (clones the object) + xyz::protocol> p_subset(p); + EXPECT_FALSE(p.valueless_after_move()); + EXPECT_FALSE(p_subset.valueless_after_move()); + EXPECT_EQ(p.name(), "ALike"); + EXPECT_EQ(p_subset.name(), "ALike"); +} + +TEST(ProtocolTest, CountAllocationsForNarrowingCopyConversion) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + { + xyz::protocol> p( + std::allocator_arg, + xyz::TrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type, 42); + EXPECT_EQ(alloc_counter, 1); + EXPECT_EQ(dealloc_counter, 0); + + // Convert via copy constructor - this should clone the underlying object + xyz::protocol> p_subset(p); + EXPECT_FALSE(p.valueless_after_move()); + EXPECT_FALSE(p_subset.valueless_after_move()); + EXPECT_EQ(alloc_counter, 2); + EXPECT_EQ(dealloc_counter, 0); + } + EXPECT_EQ(alloc_counter, 2); + EXPECT_EQ(dealloc_counter, 2); +} + +TEST(ProtocolTest, NarrowingCopyConversionFromValueless) { + xyz::protocol> p(std::in_place_type, + 42); + xyz::protocol> p2 = std::move(p); + EXPECT_TRUE(p.valueless_after_move()); + + // Copying a valueless protocol should yield a valueless protocol + xyz::protocol> p_subset(p); + EXPECT_TRUE(p_subset.valueless_after_move()); +} + +TEST(ProtocolTest, NarrowingCopyConversionNonEqualAllocators) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + { + xyz::protocol> p( + std::allocator_arg, + NonEqualTrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type, 42); + EXPECT_EQ(alloc_counter, 1); + EXPECT_EQ(dealloc_counter, 0); + + // Copy-convert with different allocator instance (non-equal allocators) + NonEqualTrackingAllocator target_alloc(&alloc_counter, + &dealloc_counter); + xyz::protocol> p_subset( + std::allocator_arg, target_alloc, p); + + EXPECT_FALSE(p.valueless_after_move()); + EXPECT_FALSE(p_subset.valueless_after_move()); + EXPECT_EQ(p.name(), "ALike"); + EXPECT_EQ(p_subset.name(), "ALike"); + + // 1 allocation for source object, plus 1 allocation for cloned object on + // the target allocator + EXPECT_EQ(alloc_counter, 2); + EXPECT_EQ(dealloc_counter, 0); + } + // All allocations should be cleaned up + EXPECT_EQ(alloc_counter, 2); + EXPECT_EQ(dealloc_counter, 2); +} + +TEST(ProtocolTest, NarrowingMoveConversionNonEqualAllocators) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + { + xyz::protocol> p( + std::allocator_arg, + NonEqualTrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type, 42); + EXPECT_EQ(alloc_counter, 1); + EXPECT_EQ(dealloc_counter, 0); + + // Convert with different allocator instances (non-equal allocators) + // This will force allocating a copy of the underlying object using the + // destination's allocator + xyz::protocol> p_subset( + std::move(p)); + + EXPECT_FALSE(p_subset.valueless_after_move()); + EXPECT_EQ(p_subset.name(), "ALike"); + // Since allocators compare non-equal, it must have allocated on the new + // allocator, and old remains valid/valueless? Wait, the standard move + // constructor for different allocators will move-construct the content, + // which allocates 1 and destroys/deallocates the source object. + EXPECT_EQ(alloc_counter, 2); + } + // All allocations should be cleaned up + EXPECT_EQ(alloc_counter, 2); + EXPECT_EQ(dealloc_counter, 2); +} + +TEST(ProtocolTest, CountAllocationsForNarrowingMoveConversion) { + unsigned alloc_counter = 0; + unsigned dealloc_counter = 0; + { + xyz::protocol> a( + std::allocator_arg, + xyz::TrackingAllocator(&alloc_counter, &dealloc_counter), + std::in_place_type, 42); + EXPECT_EQ(alloc_counter, 1); + EXPECT_EQ(dealloc_counter, 0); + + // Perform narrowing move conversion with equal allocators (stolen pointer) + xyz::protocol> aa( + std::move(a)); + + EXPECT_TRUE(a.valueless_after_move()); + EXPECT_FALSE(aa.valueless_after_move()); + // Zero new allocations and zero deallocations during conversion + EXPECT_EQ(alloc_counter, 1); + EXPECT_EQ(dealloc_counter, 0); + } + // The underlying object is destroyed at block exit + EXPECT_EQ(alloc_counter, 1); + EXPECT_EQ(dealloc_counter, 1); +} + +TEST(ProtocolTest, NarrowingMoveConversionFromValueless) { + xyz::protocol> p(std::in_place_type, + 42); + xyz::protocol> p2 = std::move(p); + EXPECT_TRUE(p.valueless_after_move()); + + // Converting a valueless protocol should yield a valueless protocol + xyz::protocol> p_subset = + std::move(p); + EXPECT_TRUE(p_subset.valueless_after_move()); +} + +TEST(ProtocolViewTest, NarrowingConversionCombinations) { + ALike a_obj; + xyz::protocol_view view_a(a_obj); + + // 1. Convert mutable view of A to const view of A_Subset + xyz::protocol_view const_view_subset = view_a; + EXPECT_EQ(const_view_subset.name(), "ALike"); + + // 2. Convert mutable view of A to mutable view of A_Subset + xyz::protocol_view view_subset = view_a; + EXPECT_EQ(view_subset.name(), "ALike"); + + // 3. Convert const view of A to const view of A_Subset + xyz::protocol_view const_view_a(a_obj); + xyz::protocol_view const_view_subset2 = const_view_a; + EXPECT_EQ(const_view_subset2.name(), "ALike"); +} + +TEST(ProtocolTest, NarrowingConversionConcurrentAccess) { + constexpr int kNumThreads = 10; + std::vector threads; + std::atomic start_signal{false}; + + for (int i = 0; i < kNumThreads; ++i) { + threads.emplace_back([&start_signal]() { + while (!start_signal.load()) { + std::this_thread::yield(); + } + ALike a_obj; + xyz::protocol_view view_a(a_obj); + + // Perform view conversions concurrently + xyz::protocol_view const_view = view_a; + EXPECT_EQ(const_view.name(), "ALike"); + + xyz::protocol_view mut_view = view_a; + EXPECT_EQ(mut_view.name(), "ALike"); + + // Perform owning conversions concurrently + xyz::protocol> p( + std::in_place_type, 42); + xyz::protocol> p_subset = + std::move(p); + EXPECT_EQ(p_subset.name(), "ALike"); + }); + } + + // Release all threads to run concurrently + start_signal.store(true); + + for (auto& t : threads) { + t.join(); + } +} + +TEST(ProtocolTest, NarrowingConversionConcurrentStressing) { + constexpr int kNumThreads = 20; + constexpr int kIterationsPerThread = 50; + std::vector threads; + std::atomic start_signal{false}; + + for (int i = 0; i < kNumThreads; ++i) { + threads.emplace_back([&start_signal]() { + while (!start_signal.load()) { + std::this_thread::yield(); + } + for (int iter = 0; iter < kIterationsPerThread; ++iter) { + ALike a_obj; + xyz::protocol_view view_a(a_obj); + + // Concurrently query view conversions (often hitting cache) + xyz::protocol_view const_view = view_a; + EXPECT_EQ(const_view.name(), "ALike"); + + xyz::protocol_view mut_view = view_a; + EXPECT_EQ(mut_view.name(), "ALike"); + + // Concurrently query owning conversions (often hitting cache) + xyz::protocol> p( + std::in_place_type, 42); + xyz::protocol> p_subset = + std::move(p); + EXPECT_EQ(p_subset.name(), "ALike"); + + // Use custom allocator to trigger a different map entry + xyz::protocol> p_char( + std::in_place_type, 42); + xyz::protocol> p_subset_char = + std::move(p_char); + EXPECT_EQ(p_subset_char.name(), "ALike"); + } + }); + } + + // Release all threads to run concurrently and contend on cache + // lookup/creation + start_signal.store(true); + + for (auto& t : threads) { + t.join(); + } +} + } // namespace diff --git a/scripts/protocol.j2 b/scripts/protocol.j2 index 6215f31..eb533ba 100644 --- a/scripts/protocol.j2 +++ b/scripts/protocol.j2 @@ -115,12 +115,57 @@ inline constexpr view_vtable_{{ c.name }} view_vtable_{{ c.name }}_for = { {% endfor %} }; +template <> +struct protocol_vtable_traits<{{ full_class_name }}> { + using const_vtable = const_view_vtable_{{ c.name }}; + using vtable = view_vtable_{{ c.name }}; +}; + +template +struct protocol_owning_vtable_traits<{{ full_class_name }}, Allocator> { + using vtable = typename protocol<{{ full_class_name }}, Allocator>::vtable; +}; + +template +inline void map_vtable_members(const From* from, const_view_vtable_{{ c.name }}* to) { +{% for m in c.methods %}{% if m.is_const %} + to->{{ m.name | mangle }}_{{ method_guids[loop.index0] }} = from->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; +{% endif %}{% endfor %} +} + +template +inline void map_mutable_vtable_members(const From* from, view_vtable_{{ c.name }}* to) { +{% for m in c.methods %}{% if m.is_const %} + to->const_view.{{ m.name | mangle }}_{{ method_guids[loop.index0] }} = from->const_view.{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; +{% endif %}{% endfor %} +{% for m in c.methods %}{% if not m.is_const %} + to->{{ m.name | mangle }}_{{ method_guids[loop.index0] }} = from->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; +{% endif %}{% endfor %} +} + +template +inline void map_owning_vtable_members(const From* from, typename protocol_owning_vtable_traits<{{ full_class_name }}, typename From::allocator_type>::vtable* to) { + to->xyz_protocol_clone = from->xyz_protocol_clone; + to->xyz_protocol_move = from->xyz_protocol_move; + to->xyz_protocol_destroy = from->xyz_protocol_destroy; + to->view_vt = get_or_create_mutable_vtable(from->view_vt); +{% for m in c.methods %} + to->{{ m.name | mangle }}_{{ method_guids[loop.index0] }} = from->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; +{% endfor %} +} + template class protocol<{{ full_class_name }}, Allocator> { friend class protocol_view<{{ full_class_name }}>; friend class protocol_view; + template + friend class protocol; + template + friend struct protocol_owning_vtable_traits; struct vtable { + using protocol_type = {{ full_class_name }}; + using allocator_type = Allocator; void* (*xyz_protocol_clone)(void* cb, const Allocator& alloc); void* (*xyz_protocol_move)(void* cb, const Allocator& alloc); void (*xyz_protocol_destroy)(void* cb, const Allocator& alloc); @@ -232,6 +277,54 @@ class protocol<{{ full_class_name }}, Allocator> { [[no_unique_address]] Allocator alloc_; public: + template + requires(!std::same_as) + constexpr protocol(protocol&& other) noexcept( + allocator_traits::is_always_equal::value) + : alloc_(other.alloc_) { + if (alloc_ == other.alloc_) { + p_ = std::exchange(other.p_, nullptr); + vtable_ = get_or_create_owning_vtable( + std::exchange(other.vtable_, nullptr) + ); + } else { + if (!other.valueless_after_move()) { + p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_); + vtable_ = get_or_create_owning_vtable(other.vtable_); + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + } + + template + requires(!std::same_as) + constexpr protocol(const protocol& other) + : alloc_(allocator_traits::select_on_container_copy_construction(other.alloc_)) { + if (!other.valueless_after_move()) { + p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); + vtable_ = get_or_create_owning_vtable(other.vtable_); + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + + template + requires(!std::same_as) + constexpr protocol(std::allocator_arg_t, const Allocator& alloc, + const protocol& other) + : alloc_(alloc) { + if (!other.valueless_after_move()) { + p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); + vtable_ = get_or_create_owning_vtable(other.vtable_); + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + explicit constexpr protocol() requires std::default_initializable<{{ full_class_name }}> && protocol_concept_{{ c.name }}<{{ full_class_name }}> && std::copy_constructible<{{ full_class_name }}> @@ -240,6 +333,7 @@ class protocol<{{ full_class_name }}, Allocator> { template constexpr explicit protocol(U&& u) requires(!std::same_as>) && + not_protocol_or_view && std::copy_constructible> && protocol_concept_{{ c.name }} : protocol(std::allocator_arg_t{}, Allocator{}, std::forward(u)) {} @@ -247,6 +341,7 @@ class protocol<{{ full_class_name }}, Allocator> { template explicit constexpr protocol(std::in_place_type_t, Ts&&... ts) requires std::same_as, U> && + not_protocol_or_view && std::constructible_from && std::copy_constructible && std::default_initializable && protocol_concept_{{ c.name }} @@ -257,6 +352,7 @@ class protocol<{{ full_class_name }}, Allocator> { explicit constexpr protocol(std::in_place_type_t, std::initializer_list ilist, Ts&&... ts) requires std::same_as, U> && + not_protocol_or_view && std::constructible_from, Ts&&...> && std::copy_constructible && std::default_initializable && protocol_concept_{{ c.name }} @@ -284,6 +380,7 @@ class protocol<{{ full_class_name }}, Allocator> { constexpr explicit protocol(std::allocator_arg_t, const Allocator& alloc, U&& u) requires(not std::same_as>) && + not_protocol_or_view && std::copy_constructible> && protocol_concept_{{ c.name }} : alloc_(alloc) { @@ -295,6 +392,7 @@ class protocol<{{ full_class_name }}, Allocator> { explicit constexpr protocol(std::allocator_arg_t, const Allocator& alloc, std::in_place_type_t, Ts&&... ts) requires std::same_as, U> && + not_protocol_or_view && std::constructible_from && std::copy_constructible && protocol_concept_{{ c.name }} : alloc_(alloc) { @@ -307,6 +405,7 @@ class protocol<{{ full_class_name }}, Allocator> { std::in_place_type_t, std::initializer_list ilist, Ts&&... ts) requires std::same_as, U> && + not_protocol_or_view && std::constructible_from, Ts&&...> && std::copy_constructible && protocol_concept_{{ c.name }} : alloc_(alloc) { @@ -400,7 +499,8 @@ class protocol<{{ full_class_name }}, Allocator> { template <> class protocol_view { - friend class protocol_view<{{ full_class_name }}>; + template + friend class protocol_view; const void* ptr_; const const_view_vtable_{{ c.name }}* vptr_; @@ -419,16 +519,14 @@ class protocol_view { public: template requires protocol_const_concept_{{ c.name }} && - (!std::same_as, protocol_view<{{ full_class_name }}> >) && - (!std::same_as, protocol_view >) + not_protocol_or_view constexpr protocol_view(const T& obj) noexcept : ptr_(std::addressof(obj)), vptr_(&const_view_vtable_{{ c.name }}_for>) {} template requires protocol_const_concept_{{ c.name }} && - (!std::same_as, protocol_view<{{ full_class_name }}> >) && - (!std::same_as, protocol_view >) + not_protocol_or_view protocol_view(const T&&) = delete; template @@ -444,6 +542,18 @@ class protocol_view { : ptr_(checked_ptr(p)), vptr_(&p.vtable_->view_vt->const_view) {} + template + requires(!std::same_as) + protocol_view(const protocol_view& other) + : ptr_(other.ptr_), + vptr_(get_or_create_vtable(other.vptr_)) {} + + template + requires(!std::same_as) + protocol_view(const protocol_view& other) + : ptr_(other.ptr_), + vptr_(get_or_create_vtable(&other.vptr_->const_view)) {} + {% for m in c.methods %}{% if m.is_const %} {% set params = [] %} {% set passes = [] %} @@ -461,6 +571,9 @@ class protocol_view { template <> class protocol_view<{{ full_class_name }}> { + template + friend class protocol_view; + void* ptr_; const view_vtable_{{ c.name }}* vptr_; @@ -473,8 +586,7 @@ class protocol_view<{{ full_class_name }}> { public: template requires protocol_concept_{{ c.name }} && - (!std::same_as, protocol_view<{{ full_class_name }}> >) && - (!std::same_as, protocol_view >) + not_protocol_or_view constexpr protocol_view(T& obj) noexcept : ptr_(std::addressof(obj)), vptr_(&view_vtable_{{ c.name }}_for>) {} @@ -484,6 +596,12 @@ class protocol_view<{{ full_class_name }}> { : ptr_(checked_ptr(p)), vptr_(p.vtable_->view_vt) {} + template + requires(!std::same_as) + protocol_view(const protocol_view& other) + : ptr_(other.ptr_), + vptr_(get_or_create_mutable_vtable(other.vptr_)) {} + constexpr operator protocol_view() const noexcept { return protocol_view{static_cast(ptr_), &vptr_->const_view}; } diff --git a/scripts/test_concept_errors.py b/scripts/test_concept_errors.py index 8c8d165..9aa8c2e 100644 --- a/scripts/test_concept_errors.py +++ b/scripts/test_concept_errors.py @@ -234,3 +234,77 @@ class BadALike_NoExceptViolation { } """ compile_check(source, [r"protocol_concept_A", r"name\(\)"]) + + +def test_invalid_view_widening(compile_check: Callable[[str, List[str]], None]) -> None: + """ + Verify that a protocol_view cannot be widened to a protocol_view with more methods. + + Specifically, from A_Subset to A. + """ + source = """ + #include "generated/protocol_A.h" + #include "generated/protocol_A_Subset.h" + #include "interface_A.h" + #include "interface_A_Subset.h" + + void test(xyz::protocol_view view_subset) { + xyz::protocol_view view_a(view_subset); + } + """ + compile_check(source, [r"no member named|no matching function for call"]) + + +def test_invalid_view_constness_narrowing( + compile_check: Callable[[str, List[str]], None], +) -> None: + """ + Verify that a const protocol_view cannot be converted to a mutable protocol_view. + + Even with narrowing (e.g. from const A to A_Subset). + """ + source = """ + #include "generated/protocol_A.h" + #include "generated/protocol_A_Subset.h" + #include "interface_A.h" + #include "interface_A_Subset.h" + + void test(xyz::protocol_view view_const_a) { + xyz::protocol_view view_subset(view_const_a); + } + """ + compile_check( + source, + [ + r"no matching function for call|no matching constructor|" + r"cannot convert|no matching template for|template argument deduction" + ], + ) + + +def test_invalid_protocol_widening( + compile_check: Callable[[str, List[str]], None], +) -> None: + """ + Verify that an owning protocol cannot be widened to a protocol with more methods. + + Specifically, from A_Subset to A. + """ + source = """ + #include "generated/protocol_A.h" + #include "generated/protocol_A_Subset.h" + #include "interface_A.h" + #include "interface_A_Subset.h" + + void test(xyz::protocol> p_subset) { + xyz::protocol> p(std::move(p_subset)); + } + """ + compile_check( + source, + [ + r"no matching function for call|no matching constructor|" + r"cannot convert|static assertion failed|no matching template for|" + r"template argument deduction|no member named|has no member" + ], + ) From 29a41fba56bbaf5b52bc74020bfd8f3a643e7852 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 17:50:30 +0100 Subject: [PATCH 07/11] Fix merge-induced problems --- CMakeLists.txt | 3 -- scripts/protocol.j2 | 123 -------------------------------------------- 2 files changed, 126 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d6bdb8..ce30b66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,17 +110,14 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) CLASS_NAME B INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_B.h HEADER interface_B.h OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_B.h) - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_B.h) xyz_generate_protocol( CLASS_NAME C INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_C.h HEADER interface_C.h OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_C.h) - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_C.h) xyz_generate_protocol( CLASS_NAME D INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface_D.h HEADER interface_D.h OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_D.h) - OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/protocol_D.h) add_custom_target( generate_protocols diff --git a/scripts/protocol.j2 b/scripts/protocol.j2 index 597ed26..eb533ba 100644 --- a/scripts/protocol.j2 +++ b/scripts/protocol.j2 @@ -51,30 +51,21 @@ concept protocol_concept_{{ c.name }} = protocol_const_concept_{{ c.name }}{% {% endfor %} }{% endif %}; -struct const_view_vtable_{{ c.name }} { struct const_view_vtable_{{ c.name }} { {% for m in c.methods %}{% if m.is_const %} {% set params = [] %} {% for a in m.arguments %}{% set _ = params.append(a.type.name) %}{% endfor %} - {% for a in m.arguments %}{% set _ = params.append(a.type.name) %}{% endfor %} {% set params_str = params | join(", ") %} {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(const void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; - {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(const void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; {% endif %}{% endfor %} }; -{% set const_methods = [] %} -{% set const_method_indices = [] %} -{% for m in c.methods %}{% if m.is_const %}{% set _ = const_methods.append(m) %}{% set _ = const_method_indices.append(loop.index0) %}{% endif %}{% endfor %} {% set const_methods = [] %} {% set const_method_indices = [] %} {% for m in c.methods %}{% if m.is_const %}{% set _ = const_methods.append(m) %}{% set _ = const_method_indices.append(loop.index0) %}{% endif %}{% endfor %} template inline constexpr const_view_vtable_{{ c.name }} const_view_vtable_{{ c.name }}_for = { -{% for m in const_methods %} - {% set i = const_method_indices[loop.index0] %} -inline constexpr const_view_vtable_{{ c.name }} const_view_vtable_{{ c.name }}_for = { {% for m in const_methods %} {% set i = const_method_indices[loop.index0] %} {% set params = [] %} @@ -85,11 +76,9 @@ inline constexpr const_view_vtable_{{ c.name }} const_view_vtable_{{ c.name }}_f {% endfor %} {% set params_str = params | join(", ") %} {% set passes_str = passes | join(", ") %} - [](const void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} -> {{ m.return_type.name }} { [](const void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} -> {{ m.return_type.name }} { {% if m.return_type.name != 'void' %}return {% endif %}static_cast(ptr)->{{ m.name }}({{ passes_str }}); }{% if not loop.last %},{% endif %} - }{% if not loop.last %},{% endif %} {% endfor %} }; @@ -103,28 +92,11 @@ struct view_vtable_{{ c.name }} { {% endif %}{% endfor %} }; -{% set non_const_methods = [] %} -{% set non_const_method_indices = [] %} -{% for m in c.methods %}{% if not m.is_const %}{% set _ = non_const_methods.append(m) %}{% set _ = non_const_method_indices.append(loop.index0) %}{% endif %}{% endfor %} -struct view_vtable_{{ c.name }} { - const_view_vtable_{{ c.name }} const_view; -{% for m in c.methods %}{% if not m.is_const %} - {% set params = [] %} - {% for a in m.arguments %}{% set _ = params.append(a.type.name) %}{% endfor %} - {% set params_str = params | join(", ") %} - {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(void* ptr{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; -{% endif %}{% endfor %} -}; - {% set non_const_methods = [] %} {% set non_const_method_indices = [] %} {% for m in c.methods %}{% if not m.is_const %}{% set _ = non_const_methods.append(m) %}{% set _ = non_const_method_indices.append(loop.index0) %}{% endif %}{% endfor %} template -inline constexpr view_vtable_{{ c.name }} view_vtable_{{ c.name }}_for = { - const_view_vtable_{{ c.name }}_for{% if non_const_methods %},{% endif %} -{% for m in non_const_methods %} - {% set i = non_const_method_indices[loop.index0] %} inline constexpr view_vtable_{{ c.name }} view_vtable_{{ c.name }}_for = { const_view_vtable_{{ c.name }}_for{% if non_const_methods %},{% endif %} {% for m in non_const_methods %} @@ -202,67 +174,44 @@ class protocol<{{ full_class_name }}, Allocator> { {% set params = [] %} {% for a in m.arguments %} {% set _ = params.append(a.type.name) %} - {% set _ = params.append(a.type.name) %} {% endfor %} {% set params_str = params | join(", ") %} {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; - {{ m.return_type.name }} (*{{ m.name | mangle }}_{{ method_guids[loop.index0] }})(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %}; {% endfor %} }; template - struct vtable_impl { - using t_allocator = typename std::allocator_traits< - Allocator>::template rebind_alloc; - using t_alloc_traits = std::allocator_traits; struct vtable_impl { using t_allocator = typename std::allocator_traits< Allocator>::template rebind_alloc; using t_alloc_traits = std::allocator_traits; - static void* xyz_protocol_clone(void* cb, const Allocator& alloc) { - auto* self = static_cast(cb); - t_allocator t_alloc(alloc); - auto mem = t_alloc_traits::allocate(t_alloc, 1); static void* xyz_protocol_clone(void* cb, const Allocator& alloc) { auto* self = static_cast(cb); t_allocator t_alloc(alloc); auto mem = t_alloc_traits::allocate(t_alloc, 1); try { - t_alloc_traits::construct(t_alloc, mem, *self); t_alloc_traits::construct(t_alloc, mem, *self); return mem; } catch (...) { - t_alloc_traits::deallocate(t_alloc, mem, 1); t_alloc_traits::deallocate(t_alloc, mem, 1); throw; } } - static void* xyz_protocol_move(void* cb, const Allocator& alloc) { - auto* self = static_cast(cb); - t_allocator t_alloc(alloc); - auto mem = t_alloc_traits::allocate(t_alloc, 1); static void* xyz_protocol_move(void* cb, const Allocator& alloc) { auto* self = static_cast(cb); t_allocator t_alloc(alloc); auto mem = t_alloc_traits::allocate(t_alloc, 1); try { - t_alloc_traits::construct(t_alloc, mem, std::move(*self)); t_alloc_traits::construct(t_alloc, mem, std::move(*self)); return mem; } catch (...) { - t_alloc_traits::deallocate(t_alloc, mem, 1); t_alloc_traits::deallocate(t_alloc, mem, 1); throw; } } - static void xyz_protocol_destroy(void* cb, const Allocator& alloc) { - auto* self = static_cast(cb); - t_allocator t_alloc(alloc); - t_alloc_traits::destroy(t_alloc, self); - t_alloc_traits::deallocate(t_alloc, self, 1); static void xyz_protocol_destroy(void* cb, const Allocator& alloc) { auto* self = static_cast(cb); t_allocator t_alloc(alloc); @@ -284,19 +233,11 @@ class protocol<{{ full_class_name }}, Allocator> { auto* self = static_cast(cb); self->{{ m.name }}({{ passes_str }}); } - static {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} { - auto* self = static_cast(cb); - self->{{ m.name }}({{ passes_str }}); - } {% else %} static {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} { auto* self = static_cast(cb); return self->{{ m.name }}({{ passes_str }}); } - static {{ m.return_type.name }} {{ m.name | mangle }}_{{ method_guids[loop.index0] }}(void* cb{% if params %}, {% endif %}{{ params_str }}){% if m.is_noexcept %} noexcept{% endif %} { - auto* self = static_cast(cb); - return self->{{ m.name }}({{ passes_str }}); - } {% endif %} {% endfor %} @@ -309,22 +250,11 @@ class protocol<{{ full_class_name }}, Allocator> { {{ m.name | mangle }}_{{ method_guids[loop.index0] }}{% if not loop.last %},{% endif %} {% endfor %} }; - - static constexpr vtable vtable_ = { - xyz_protocol_clone, - xyz_protocol_move, - xyz_protocol_destroy, - &view_vtable_{{ c.name }}_for, -{% for m in c.methods %} - {{ m.name | mangle }}_{{ method_guids[loop.index0] }}{% if not loop.last %},{% endif %} -{% endfor %} - }; }; using allocator_traits = std::allocator_traits; template - [[nodiscard]] constexpr void* create_storage( [[nodiscard]] constexpr void* create_storage( Ts&&... ts) const { using t_allocator = typename std::allocator_traits< @@ -332,25 +262,16 @@ class protocol<{{ full_class_name }}, Allocator> { t_allocator t_alloc(alloc_); using t_alloc_traits = std::allocator_traits; auto mem = t_alloc_traits::allocate(t_alloc, 1); - using t_allocator = typename std::allocator_traits< - Allocator>::template rebind_alloc; - t_allocator t_alloc(alloc_); - using t_alloc_traits = std::allocator_traits; - auto mem = t_alloc_traits::allocate(t_alloc, 1); try { - t_alloc_traits::construct(t_alloc, mem, t_alloc_traits::construct(t_alloc, mem, std::forward(ts)...); return mem; } catch (...) { - t_alloc_traits::deallocate(t_alloc, mem, 1); t_alloc_traits::deallocate(t_alloc, mem, 1); throw; } } - void* p_; - const vtable* vtable_; void* p_; const vtable* vtable_; [[no_unique_address]] Allocator alloc_; @@ -453,8 +374,6 @@ class protocol<{{ full_class_name }}, Allocator> { : alloc_(alloc) { p_ = create_storage<{{ full_class_name }}>(); vtable_ = &vtable_impl<{{ full_class_name }}>::vtable_; - p_ = create_storage<{{ full_class_name }}>(); - vtable_ = &vtable_impl<{{ full_class_name }}>::vtable_; } template @@ -467,8 +386,6 @@ class protocol<{{ full_class_name }}, Allocator> { : alloc_(alloc) { p_ = create_storage>(std::forward(u)); vtable_ = &vtable_impl>::vtable_; - p_ = create_storage>(std::forward(u)); - vtable_ = &vtable_impl>::vtable_; } template @@ -481,8 +398,6 @@ class protocol<{{ full_class_name }}, Allocator> { : alloc_(alloc) { p_ = create_storage(std::forward(ts)...); vtable_ = &vtable_impl::vtable_; - p_ = create_storage(std::forward(ts)...); - vtable_ = &vtable_impl::vtable_; } template @@ -496,8 +411,6 @@ class protocol<{{ full_class_name }}, Allocator> { : alloc_(alloc) { p_ = create_storage(ilist, std::forward(ts)...); vtable_ = &vtable_impl::vtable_; - p_ = create_storage(ilist, std::forward(ts)...); - vtable_ = &vtable_impl::vtable_; } constexpr protocol(std::allocator_arg_t, const Allocator& alloc, @@ -506,13 +419,9 @@ class protocol<{{ full_class_name }}, Allocator> { if (!other.valueless_after_move()) { p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); vtable_ = other.vtable_; - p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); - vtable_ = other.vtable_; } else { p_ = nullptr; vtable_ = nullptr; - p_ = nullptr; - vtable_ = nullptr; } } @@ -523,25 +432,17 @@ class protocol<{{ full_class_name }}, Allocator> { if constexpr (allocator_traits::is_always_equal::value) { p_ = std::exchange(other.p_, nullptr); vtable_ = std::exchange(other.vtable_, nullptr); - p_ = std::exchange(other.p_, nullptr); - vtable_ = std::exchange(other.vtable_, nullptr); } else { if (alloc_ == other.alloc_) { p_ = std::exchange(other.p_, nullptr); vtable_ = std::exchange(other.vtable_, nullptr); - p_ = std::exchange(other.p_, nullptr); - vtable_ = std::exchange(other.vtable_, nullptr); } else { if (!other.valueless_after_move()) { p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_); vtable_ = other.vtable_; - p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_); - vtable_ = other.vtable_; } else { p_ = nullptr; vtable_ = nullptr; - p_ = nullptr; - vtable_ = nullptr; } } } @@ -549,12 +450,9 @@ class protocol<{{ full_class_name }}, Allocator> { constexpr bool valueless_after_move() const noexcept { return p_ == nullptr; - return p_ == nullptr; } ~protocol() { - if (p_ != nullptr) { - vtable_->xyz_protocol_destroy(p_, alloc_); if (p_ != nullptr) { vtable_->xyz_protocol_destroy(p_, alloc_); } @@ -564,8 +462,6 @@ class protocol<{{ full_class_name }}, Allocator> { allocator_traits::is_always_equal::value) { std::swap(p_, other.p_); std::swap(vtable_, other.vtable_); - std::swap(p_, other.p_); - std::swap(vtable_, other.vtable_); if constexpr (!allocator_traits::is_always_equal::value) { std::swap(alloc_, other.alloc_); } @@ -576,8 +472,6 @@ class protocol<{{ full_class_name }}, Allocator> { allocator_traits::is_always_equal::value) { std::swap(p_, other.p_); std::swap(vtable_, other.vtable_); - std::swap(p_, other.p_); - std::swap(vtable_, other.vtable_); if constexpr (!allocator_traits::is_always_equal::value) { std::swap(alloc_, other.alloc_); } @@ -599,7 +493,6 @@ class protocol<{{ full_class_name }}, Allocator> { {% set params_str = params | join(", ") %} {% set passes_str = passes | join(", ") %} {{ m.return_type.name }} {{ m.name }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} { return vtable_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(p_{% if passes %}, {% endif %}{{ passes_str }}); } - {{ m.return_type.name }} {{ m.name }}({{ params_str }}){% if m.is_const %} const{% endif %}{% if m.is_noexcept %} noexcept{% endif %} { return vtable_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(p_{% if passes %}, {% endif %}{{ passes_str }}); } {% endfor %} }; @@ -611,12 +504,9 @@ class protocol_view { const void* ptr_; const const_view_vtable_{{ c.name }}* vptr_; - const const_view_vtable_{{ c.name }}* vptr_; constexpr protocol_view(const void* ptr, const const_view_vtable_{{ c.name }}* vptr) noexcept - : ptr_(ptr), vptr_(vptr) {} - const const_view_vtable_{{ c.name }}* vptr) noexcept : ptr_(ptr), vptr_(vptr) {} template @@ -624,7 +514,6 @@ class protocol_view { const protocol<{{ full_class_name }}, Alloc>& p) noexcept { assert(!p.valueless_after_move()); return p.p_; - return p.p_; } public: @@ -634,7 +523,6 @@ class protocol_view { constexpr protocol_view(const T& obj) noexcept : ptr_(std::addressof(obj)), vptr_(&const_view_vtable_{{ c.name }}_for>) {} - vptr_(&const_view_vtable_{{ c.name }}_for>) {} template requires protocol_const_concept_{{ c.name }} && @@ -645,7 +533,6 @@ class protocol_view { protocol_view(const protocol<{{ full_class_name }}, Alloc>& p) noexcept : ptr_(checked_ptr(p)), vptr_(&p.vtable_->view_vt->const_view) {} - vptr_(&p.vtable_->view_vt->const_view) {} template protocol_view(const protocol<{{ full_class_name }}, Alloc>&&) = delete; @@ -678,7 +565,6 @@ class protocol_view { {% set passes_str = passes | join(", ") %} {{ m.return_type.name }} {{ m.name }}({{ params_str }}) const{% if m.is_noexcept %} noexcept{% endif %} { {% if m.return_type.name != 'void' %}return {% endif %}vptr_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); - {% if m.return_type.name != 'void' %}return {% endif %}vptr_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); } {% endif %}{% endfor %} }; @@ -690,13 +576,11 @@ class protocol_view<{{ full_class_name }}> { void* ptr_; const view_vtable_{{ c.name }}* vptr_; - const view_vtable_{{ c.name }}* vptr_; template static void* checked_ptr(protocol<{{ full_class_name }}, Alloc>& p) noexcept { assert(!p.valueless_after_move()); return p.p_; - return p.p_; } public: @@ -706,7 +590,6 @@ class protocol_view<{{ full_class_name }}> { constexpr protocol_view(T& obj) noexcept : ptr_(std::addressof(obj)), vptr_(&view_vtable_{{ c.name }}_for>) {} - vptr_(&view_vtable_{{ c.name }}_for>) {} template protocol_view(protocol<{{ full_class_name }}, Alloc>& p) noexcept @@ -721,7 +604,6 @@ class protocol_view<{{ full_class_name }}> { constexpr operator protocol_view() const noexcept { return protocol_view{static_cast(ptr_), &vptr_->const_view}; - return protocol_view{static_cast(ptr_), &vptr_->const_view}; } {% for m in c.methods %} @@ -739,11 +621,6 @@ class protocol_view<{{ full_class_name }}> { {% else %} {% if m.return_type.name != 'void' %}return {% endif %}vptr_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); {% endif %} - {% if m.is_const %} - {% if m.return_type.name != 'void' %}return {% endif %}vptr_->const_view.{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); - {% else %} - {% if m.return_type.name != 'void' %}return {% endif %}vptr_->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}(ptr_{% if passes %}, {% endif %}{{ passes_str }}); - {% endif %} } {% endfor %} }; From b6383e6da181007708fe59a4586de7b85483b850 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 17:55:28 +0100 Subject: [PATCH 08/11] CMake cleanup --- CMakeLists.txt | 13 ++++++------- cmake/xyz_add_library.cmake | 38 +++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ce30b66..0626252 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,12 +69,12 @@ include(xyz_generate_protocol) find_package(ClangTidy) find_package(IWYU) -xyz_add_library(NAME protocol ALIAS xyz_protocol::protocol) +xyz_add_library( + NAME protocol + ALIAS xyz_protocol::protocol + FILES protocol.cc) target_sources( - protocol INTERFACE $) - -xyz_add_object_library(NAME protocol_cc FILES protocol.cc LINK_LIBRARIES - xyz_protocol::protocol) + protocol PUBLIC $) if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) @@ -132,7 +132,6 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) protocol_test LINK_LIBRARIES protocol - protocol_cc FILES protocol_test.cc interface_A.h @@ -150,7 +149,7 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) add_executable(protocol_benchmark protocol_benchmark.cc) - target_link_libraries(protocol_benchmark PRIVATE protocol protocol_cc benchmark::benchmark) + target_link_libraries(protocol_benchmark PRIVATE protocol benchmark::benchmark) add_dependencies(protocol_benchmark generate_protocols) target_include_directories(protocol_benchmark PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/cmake/xyz_add_library.cmake b/cmake/xyz_add_library.cmake index f104172..e82062f 100644 --- a/cmake/xyz_add_library.cmake +++ b/cmake/xyz_add_library.cmake @@ -35,7 +35,7 @@ and allows configuration for common optional settings function(xyz_add_library) set(options) set(oneValueArgs NAME ALIAS VERSION) - set(multiValueArgs DEFINITIONS) + set(multiValueArgs FILES DEFINITIONS) cmake_parse_arguments(XYZ "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -58,15 +58,33 @@ function(xyz_add_library) set(XYZ_CXX_STANDARD cxx_std_${XYZ_VERSION}) endif() - add_library(${XYZ_NAME} INTERFACE) + if(XYZ_FILES) + add_library(${XYZ_NAME} STATIC) + target_sources(${XYZ_NAME} PRIVATE ${XYZ_FILES}) + target_include_directories( + ${XYZ_NAME} PUBLIC $ + $) + target_compile_features(${XYZ_NAME} PUBLIC ${XYZ_CXX_STANDARD}) + + if(XYZ_DEFINITIONS) + target_compile_definitions(${XYZ_NAME} PUBLIC ${XYZ_DEFINITIONS}) + endif() + + if(CLANG_TIDY_ENABLE AND ClangTidy_FOUND) + set_target_properties(${XYZ_NAME} PROPERTIES CXX_CLANG_TIDY "${XYZ_CLANG_TIDY}") + endif() + else() + add_library(${XYZ_NAME} INTERFACE) + target_include_directories( + ${XYZ_NAME} INTERFACE $ + $) + target_compile_features(${XYZ_NAME} INTERFACE ${XYZ_CXX_STANDARD}) + + if(XYZ_DEFINITIONS) + target_compile_definitions(${XYZ_NAME} INTERFACE ${XYZ_DEFINITIONS}) + endif() + endif() + add_library(${XYZ_ALIAS} ALIAS ${XYZ_NAME}) - target_include_directories( - ${XYZ_NAME} INTERFACE $ - $) - target_compile_features(${XYZ_NAME} INTERFACE ${XYZ_CXX_STANDARD}) - - if(XYZ_DEFINITIONS) - target_compile_definitions(${XYZ_NAME} INTERFACE ${XYZ_DEFINITIONS}) - endif(XYZ_DEFINITIONS) endfunction() From 5388b23bb420ee0fcaa7d9c9908c527b91e1e119 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 18:14:00 +0100 Subject: [PATCH 09/11] Clean up implementation notes --- implementation-notes.md | 154 +++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 73 deletions(-) diff --git a/implementation-notes.md b/implementation-notes.md index 42d842e..0727cdd 100644 --- a/implementation-notes.md +++ b/implementation-notes.md @@ -8,17 +8,17 @@ This document details the design and implementation of the `protocol` and `proto Specializations of `protocol` and `protocol_view` are generated from user-defined interface structures by [scripts/generate_protocol.py](file:///workspace/scripts/generate_protocol.py) using the Jinja2 template [scripts/protocol.j2](file:///workspace/scripts/protocol.j2). -1. **AST Parsing:** - * Uses `libclang` Python bindings (`clang.cindex`) to parse the target header file. - * Traverses the AST to construct a model of the C++ class, identifying all public non-virtual, non-template member functions. - * Extracts function attributes including: name, constness, exception specifications (`noexcept`), return types, and parameter types. - -2. **Name Mangling and Symbol Stability:** - * To prevent symbol name collisions in the generated structs, member function pointers in the vtable must be uniquely identified. - * Generates a stable suffix by computing the MD5 hash of the function signature (e.g. `func2(int,int)`) and taking the first 8 characters: - $$\text{Suffix} = \text{MD5}(\text{signature})[0..7]$$ - * Example: `int func2(int)` generates member `func2_0087aeab`. - * Pointers to these members remain stable and deterministic across compiler versions and independent generation runs. +### AST Parsing +The generator uses `libclang` Python bindings (`clang.cindex`) to parse the target header file. It traverses the AST to construct a model of the C++ class, identifying all public non-virtual, non-template member functions. It extracts function attributes including the name, constness, exception specifications (`noexcept`), return types, and parameter types. + +Interfaces must consist only of public, non-virtual, non-template member functions. Template member functions are not supported because they cannot be mapped to a fixed-size vtable struct at compile-time. During generation, the script automatically parses dependent system headers by querying the host compiler's include paths; however, custom flags must be passed to the clang parser if interfaces rely on external project headers. + +### Name Mangling and Symbol Stability +To prevent symbol name collisions in the generated structs, member function pointers in the vtable must be uniquely identified. The generator produces a stable suffix by computing the MD5 hash of the function signature (e.g. `func2(int,int)`) and taking the first 8 characters: +$$\text{Suffix} = \text{MD5}(\text{signature})[0..7]$$ +For overloaded functions, the signature string hashed to generate the suffix includes the full parameter list and constness qualifiers (for example, `write(int)const` versus `write(double)const`). This guarantees that overloaded functions produce distinct stable suffixes and separate vtable slots. + +For example, `int func2(int)` generates the member `func2_0087aeab`. Pointers to these members remain stable and deterministic across compiler versions and independent generation runs. --- @@ -26,27 +26,27 @@ Specializations of `protocol` and `protocol_view` are generated from user-define The implementation avoids compiler-generated virtual tables (`vtable`/`vptr`) to enforce value semantics, control layout constraints, and avoid runtime inheritance. Instead, it uses custom C++ structures of function pointers. -1. **Vtable Layout:** - * For each interface, the generator produces two vtable layouts: - * `const_view_vtable_`: Holds function pointers mapping const member functions. - * `view_vtable_`: Holds a nested `const_view_vtable_` member followed by function pointers for non-const member functions. - * Function pointer signatures take a type-erased pointer (`const void*` or `void*`) as the first argument, followed by the function parameters. - -2. **Vtable Specialization:** - * For a concrete type `T`, static constexpr instances `const_view_vtable_for` and `view_vtable_for` are initialized with lambdas that cast the type-erased pointer back to the concrete type: - ```cpp - [](const void* ptr, Args... args) -> Ret { - return static_cast(ptr)->member_function(args...); - } - ``` - -3. **Invocation Path:** - * `protocol_view` stores a type-erased pointer `ptr_` and a pointer to the generated vtable `vptr_`. - * Calling a member function performs a single indirection: - ```cpp - vptr_->member_function_mangled(ptr_, args...); - ``` - * Because vtable pointers point to statically allocated, immutable structs (`const_view_vtable_for`), this is identical to a standard virtual call cost but without class hierarchy coupling. +### Vtable Layout +For each interface, the generator produces two vtable layouts: +- `const_view_vtable_` holds function pointers mapping const member functions. +- `view_vtable_` holds a nested `const_view_vtable_` member followed by function pointers for non-const member functions. + +Function pointer signatures take a type-erased pointer (`const void*` or `void*`) as the first argument, followed by the function parameters. + +### Vtable Specialization +For a concrete type `T`, static constexpr instances `const_view_vtable_for` and `view_vtable_for` are initialized with lambdas that cast the type-erased pointer back to the concrete type: +```cpp +[](const void* ptr, Args... args) -> Ret { + return static_cast(ptr)->member_function(args...); +} +``` + +### Invocation Path +`protocol_view` stores a type-erased pointer `ptr_` and a pointer to the generated vtable `vptr_`. Calling a member function performs a single indirection: +```cpp +vptr_->member_function_mangled(ptr_, args...); +``` +Because vtable pointers point to statically allocated, immutable structs (`const_view_vtable_for`), this is identical to a standard virtual call cost but without class hierarchy coupling. --- @@ -54,24 +54,22 @@ The implementation avoids compiler-generated virtual tables (`vtable`/`vptr`) to A `protocol` or `protocol_view` for interface `A` can be converted to one for interface `B` if `B` is a subset (subtype) of `A`. -1. **Constructor Constraints:** - * We define type traits `is_protocol` and `is_protocol_view` along with the concept `not_protocol_or_view`. - * These traits prevent concrete constructors from matching view/protocol types during conversions, avoiding recursion or self-wrapping. - -2. **Converting Views:** - * Conversions are enabled via templated copy constructors constrained by the target vtable size and layout compatibility: - ```cpp - template - requires (!std::same_as) - constexpr protocol_view(const protocol_view& other) - : ptr_(other.ptr_), - vptr_(get_or_create_mutable_vtable(other.vptr_)) {} - ``` - -3. **Converting Owning Protocols:** - * Allocator-extended and standard converting constructors construct the target `protocol` from the source `protocol`. - * If the allocators are equal, the storage pointer `p_` is moved directly (`std::exchange`) and the target vtable is mapped. - * If the allocators are not equal, the source's `xyz_protocol_move` or `xyz_protocol_clone` function is called to construct the value in the target allocator's storage. +### Constructor Constraints +Type traits `is_protocol` and `is_protocol_view` along with the concept `not_protocol_or_view` prevent concrete constructors from matching view/protocol types during conversions, avoiding recursion or self-wrapping. + +### Converting Views +Conversions are enabled via templated copy constructors constrained by the target vtable size and layout compatibility: +```cpp +template + requires (!std::same_as) +constexpr protocol_view(const protocol_view& other) + : ptr_(other.ptr_), + vptr_(get_or_create_mutable_vtable(other.vptr_)) {} +``` +Conversions are fully transitive (for example, `protocol_view` to `protocol_view` to `protocol_view`). In each step, the registry maps the current vtable pointer to the target interface vtable. Since the mapping registry resolves type transitions directly, intermediate conversions do not create chain-linked redirects. + +### Converting Owning Protocols +Allocator-extended and standard converting constructors construct the target `protocol` from the source `protocol`. If the allocators are equal, the storage pointer `p_` is moved directly (`std::exchange`) and the target vtable is mapped. If the allocators are not equal, the source's `xyz_protocol_move` or `xyz_protocol_clone` function is called to construct the value in the target allocator's storage. --- @@ -79,26 +77,36 @@ A `protocol` or `protocol_view` for interface `A` can be converted to one for in When narrowing from `Other` to `Target`, a new vtable matching `Target`'s layout must be built and populated with function pointers extracted from `Other`'s vtable. This mapping occurs dynamically inside a global type-erased registry. -1. **Registry Signature:** - ```cpp - const void* get_or_create_vtable_erased( - const void* from_vptr, const void* type_id, std::size_t to_vtable_size, - void (*mapper)(const void* from, void* to)); - ``` - -2. **The Cache:** - * Caches mapped vtables in a static `std::unordered_map` keyed by `CacheKey{from_vptr, type_id}`. - * `type_id` is the address of a static template local `type_id_anchor`, ensuring target vtable/allocator uniqueness. - * Values are stored as `std::unique_ptr`. Because the map is node-allocated, returned pointers to elements remain stable. - -3. **Split-Lock Pattern:** - * To prevent recursive deadlocks when nested conversions occur (e.g. mapping an owning vtable requires mapping its nested mutable vtable on the same thread), the mutex is **not** held during mapping. - * **Sequence:** - 1. Lock mutex, lookup key. If found, return pointer, unlock. - 2. If miss, unlock mutex. - 3. Allocate target vtable buffer in thread-local storage. - 4. Invoke `mapper()` to populate the new vtable. - 5. Lock mutex, try to insert the buffer using `cache.emplace()`. - 6. If insertion succeeds, publish and return pointer. - 7. If insertion fails (another thread inserted the key during step 3-4), the local buffer is destroyed, and the already-cached pointer is returned. - * This guarantees that all threads always resolve to the identical vtable pointer for a given conversion key, eliminating data races and leaks under high contention. +### Registry Signature +```cpp +const void* get_or_create_vtable_erased( + const void* from_vptr, const void* type_id, std::size_t to_vtable_size, + void (*mapper)(const void* from, void* to)); +``` + +### The Cache and Lifetime Control (Intentional Leak) +Mapped vtables are cached in a static `std::unordered_map` keyed by `CacheKey{from_vptr, type_id}`. The `type_id` is the address of a static template local `type_id_anchor`, ensuring target vtable/allocator uniqueness. Values are stored as `std::unique_ptr`. Because the map is node-allocated, returned pointers to elements remain stable. + +To ensure safety during program shutdown, the cache map and its protecting mutex are initialized as dynamic objects allocated via `new` on the heap and referenced statically (`static auto& cache = *new ...`). This deliberately prevents their destruction during program termination, avoiding Undefined Behavior (such as segfaults) if other global or static objects trigger protocol conversions during cleanup/destructor execution. + +Because active references to these static structures reside in the global data segment throughout the application runtime, Address Sanitizer's Leak Sanitizer (LSAN) classifies them as reachable memory rather than a leak, passing all sanitizer checks on exit without needing suppression files. + +Since the vtables are dynamically allocated and retained on the heap until program termination, memory growth is bounded by the total number of distinct conversion type pairs in the binary. This compile-time bound ensures that the cache does not require an eviction policy (such as LRU) or memory cap, as memory consumption remains flat after startup. + +Pointer equality is used to compare the `CacheKey` components. This is safe because static vtable instances and anchor variables are guaranteed to have unique heap or data segment addresses. Compiler optimization techniques (such as COMDAT folding or duplicate variable consolidation) do not affect correctness because identical layouts that are folded share identical function pointer semantics. + +### Split-Lock Pattern +To prevent recursive deadlocks when nested conversions occur (e.g. mapping an owning vtable requires mapping its nested mutable vtable on the same thread), the mutex is not held during mapping. + +While the conversion is an O(1) pointer assignment on a cache hit, the very first conversion for a given type pair incurs a cold-start overhead due to the mutex lock, cache lookup, buffer allocation, and mapping. The conversions are therefore described as amortized zero-cost. + +The lookup and population sequence is: +1. Lock the mutex and look up the key. If found, return the pointer and unlock the mutex. +2. If it is a cache miss, unlock the mutex. +3. Allocate the target vtable buffer in thread-local storage. +4. Invoke `mapper()` to populate the new vtable. +5. Lock the mutex and attempt to insert the buffer using `cache.emplace()`. +6. If the insertion succeeds, publish and return the pointer. +7. If the insertion fails (meaning another thread inserted the key concurrently), the local buffer is destroyed, and the already-cached pointer is returned. + +This guarantees that all threads always resolve to the identical vtable pointer for a given conversion key, eliminating data races and leaks under high contention. From 9ffeb82f3c6b5ca71f0c3315ab61fc7e8883318f Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 19:24:15 +0100 Subject: [PATCH 10/11] Leak the cache to avoid issues in tear-down --- protocol.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/protocol.cc b/protocol.cc index 49855e8..b833e3b 100644 --- a/protocol.cc +++ b/protocol.cc @@ -52,9 +52,10 @@ const void* get_or_create_vtable_erased( return nullptr; } - static std::unordered_map, CacheKeyHash> - cache; - static std::mutex mutex; + static auto& cache = + *new std::unordered_map, + CacheKeyHash>(); + static auto& mutex = *new std::mutex(); CacheKey key{from_vptr, type_id}; { From 6413686ffcb4fa037f286e3d8e01257e41b0feee Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sun, 7 Jun 2026 21:28:25 +0100 Subject: [PATCH 11/11] Add comment blocks to protocol.cc --- implementation-notes.md | 11 ++++--- protocol.cc | 69 +++++++++++++++++++++++++++++------------ protocol.h | 64 ++++++++++++++++++++------------------ scripts/protocol.j2 | 16 +++++----- 4 files changed, 97 insertions(+), 63 deletions(-) diff --git a/implementation-notes.md b/implementation-notes.md index 0727cdd..640e653 100644 --- a/implementation-notes.md +++ b/implementation-notes.md @@ -64,7 +64,7 @@ template requires (!std::same_as) constexpr protocol_view(const protocol_view& other) : ptr_(other.ptr_), - vptr_(get_or_create_mutable_vtable(other.vptr_)) {} + vptr_(get_mutable_vtable(other.vptr_)) {} ``` Conversions are fully transitive (for example, `protocol_view` to `protocol_view` to `protocol_view`). In each step, the registry maps the current vtable pointer to the target interface vtable. Since the mapping registry resolves type transitions directly, intermediate conversions do not create chain-linked redirects. @@ -79,13 +79,14 @@ When narrowing from `Other` to `Target`, a new vtable matching `Target`'s layout ### Registry Signature ```cpp -const void* get_or_create_vtable_erased( - const void* from_vptr, const void* type_id, std::size_t to_vtable_size, - void (*mapper)(const void* from, void* to)); +const void* get_mapped_vtable( + const void* source_vtable_pointer, const void* conversion_anchor, + std::size_t target_vtable_size, + void (*mapping_function)(const void* source, void* target)); ``` ### The Cache and Lifetime Control (Intentional Leak) -Mapped vtables are cached in a static `std::unordered_map` keyed by `CacheKey{from_vptr, type_id}`. The `type_id` is the address of a static template local `type_id_anchor`, ensuring target vtable/allocator uniqueness. Values are stored as `std::unique_ptr`. Because the map is node-allocated, returned pointers to elements remain stable. +Mapped vtables are cached in a static `std::unordered_map` keyed by `CacheKey{source_vtable_pointer, conversion_anchor}`. The `conversion_anchor` is the address of a static template local `conversion_anchor`, ensuring target vtable/allocator uniqueness. Values are stored as `std::unique_ptr`. Because the map is node-allocated, returned pointers to elements remain stable. To ensure safety during program shutdown, the cache map and its protecting mutex are initialized as dynamic objects allocated via `new` on the heap and referenced statically (`static auto& cache = *new ...`). This deliberately prevents their destruction during program termination, avoiding Undefined Behavior (such as segfaults) if other global or static objects trigger protocol conversions during cleanup/destructor execution. diff --git a/protocol.cc b/protocol.cc index b833e3b..a631594 100644 --- a/protocol.cc +++ b/protocol.cc @@ -20,6 +20,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "protocol.h" +#include #include #include #include @@ -28,50 +29,78 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace xyz { namespace { +// The CacheKey uniquely identifies a specific type conversion mapping. +// It consists of: +// 1. source_vtable_pointer: Points to the source vtable (identifies the +// concrete type). +// 2. conversion_anchor: Points to a static tag unique to the (From, To) +// template +// instantiation. This is required because a single source vtable can be +// converted to multiple different target protocols with different vtable +// layouts, so we cannot index the cache by the source vtable address alone. struct CacheKey { - const void* from_vptr; - const void* type_id; + const void* source_vtable_pointer; + const void* conversion_anchor; bool operator==(const CacheKey&) const = default; }; struct CacheKeyHash { std::size_t operator()(const CacheKey& key) const { - std::size_t h1 = std::hash{}(key.from_vptr); - std::size_t h2 = std::hash{}(key.type_id); - return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)); + std::size_t hash_value_1 = + std::hash{}(key.source_vtable_pointer); + std::size_t hash_value_2 = std::hash{}(key.conversion_anchor); + return hash_value_1 ^ (hash_value_2 + 0x9e3779b9 + (hash_value_1 << 6) + + (hash_value_1 >> 2)); } }; } // namespace -const void* get_or_create_vtable_erased( - const void* from_vptr, const void* type_id, std::size_t to_vtable_size, - void (*mapper)(const void* from, void* to)) { - if (from_vptr == nullptr) { - return nullptr; - } - +const void* get_mapped_vtable(const void* source_vtable_pointer, + const void* conversion_anchor, + std::size_t target_vtable_size, + void (*mapping_function)(const void* source, + void* target)) { + assert(source_vtable_pointer != nullptr); + + // The cache map and its protecting mutex are allocated on the heap via 'new' + // and intentionally leaked (never destroyed). This prevents exit-time + // destruction order bugs (static destruction order fiasco) if other global + // or static objects trigger protocol conversions during program shutdown + // cleanup. + // + // Values are stored as std::unique_ptr to provide: + // 1. Dynamic sizing: Target vtable sizes are only known at runtime. + // 2. Pointer stability: Returns raw pointers that must remain valid for the + // lifetime of the application; heap allocation in std::unique_ptr ensures + // rehashing or modifying the map does not invalidate returned pointers. static auto& cache = *new std::unordered_map, CacheKeyHash>(); static auto& mutex = *new std::mutex(); - CacheKey key{from_vptr, type_id}; + CacheKey key{source_vtable_pointer, conversion_anchor}; { std::lock_guard lock(mutex); - auto it = cache.find(key); - if (it != cache.end()) { - return it->second.get(); + auto cache_iterator = cache.find(key); + if (cache_iterator != cache.end()) { + return cache_iterator->second.get(); } } - auto vtable_data = std::make_unique(to_vtable_size); - mapper(from_vptr, vtable_data.get()); + auto vtable_data = std::make_unique(target_vtable_size); + mapping_function(source_vtable_pointer, vtable_data.get()); std::lock_guard lock(mutex); - auto [inserted_it, inserted] = cache.emplace(key, std::move(vtable_data)); - return inserted_it->second.get(); + // Under the split-lock pattern, another thread might have inserted the key + // concurrently while we were mapping the vtable. std::unordered_map::emplace + // does not overwrite an existing value, so if a collision occurs, the + // existing vtable is kept, our local copy is discarded, and we return the + // stable cached pointer. + auto [inserted_iterator, inserted] = + cache.emplace(key, std::move(vtable_data)); + return inserted_iterator->second.get(); } } // namespace xyz diff --git a/protocol.h b/protocol.h index 3944d56..f4d0403 100644 --- a/protocol.h +++ b/protocol.h @@ -51,46 +51,49 @@ concept not_protocol_or_view = !is_protocol>::value && template struct protocol_vtable_traits; -const void* get_or_create_vtable_erased( - const void* from_vptr, const void* type_id, std::size_t to_vtable_size, - void (*mapper)(const void* from, void* to)); +const void* get_mapped_vtable(const void* source_vtable_pointer, + const void* conversion_anchor, + std::size_t target_vtable_size, + void (*mapping_function)(const void* source, + void* target)); template -const typename protocol_vtable_traits::const_vtable* -get_or_create_vtable( +const typename protocol_vtable_traits::const_vtable* get_vtable( const typename protocol_vtable_traits::const_vtable* - from_vptr) { + source_vtable_pointer) { using FromVtable = typename protocol_vtable_traits::const_vtable; using ToVtable = typename protocol_vtable_traits::const_vtable; - static const char type_id_anchor = 0; + static const char conversion_anchor = 0; - auto mapper = [](const void* from, void* to) { - map_vtable_members(static_cast(from), - static_cast(to)); + auto mapping_function = [](const void* source, void* target) { + map_vtable_members(static_cast(source), + static_cast(target)); }; - return static_cast(get_or_create_vtable_erased( - from_vptr, &type_id_anchor, sizeof(ToVtable), mapper)); + return static_cast( + get_mapped_vtable(source_vtable_pointer, &conversion_anchor, + sizeof(ToVtable), mapping_function)); } template -const typename protocol_vtable_traits::vtable* -get_or_create_mutable_vtable( - const typename protocol_vtable_traits::vtable* from_vptr) { +const typename protocol_vtable_traits::vtable* get_mutable_vtable( + const typename protocol_vtable_traits::vtable* + source_vtable_pointer) { using FromVtable = typename protocol_vtable_traits::vtable; using ToVtable = typename protocol_vtable_traits::vtable; - static const char type_id_anchor = 0; + static const char conversion_anchor = 0; - auto mapper = [](const void* from, void* to) { - map_mutable_vtable_members(static_cast(from), - static_cast(to)); + auto mapping_function = [](const void* source, void* target) { + map_mutable_vtable_members(static_cast(source), + static_cast(target)); }; - return static_cast(get_or_create_vtable_erased( - from_vptr, &type_id_anchor, sizeof(ToVtable), mapper)); + return static_cast( + get_mapped_vtable(source_vtable_pointer, &conversion_anchor, + sizeof(ToVtable), mapping_function)); } template @@ -98,9 +101,9 @@ struct protocol_owning_vtable_traits; template const typename protocol_owning_vtable_traits::vtable* -get_or_create_owning_vtable(const typename protocol_owning_vtable_traits< - FromProtocol, Allocator>::vtable* from_vptr) { - if (from_vptr == nullptr) { +get_owning_vtable(const typename protocol_owning_vtable_traits< + FromProtocol, Allocator>::vtable* source_vtable_pointer) { + if (source_vtable_pointer == nullptr) { return nullptr; } using FromVtable = @@ -108,15 +111,16 @@ get_or_create_owning_vtable(const typename protocol_owning_vtable_traits< using ToVtable = typename protocol_owning_vtable_traits::vtable; - static const char type_id_anchor = 0; + static const char conversion_anchor = 0; - auto mapper = [](const void* from, void* to) { - map_owning_vtable_members(static_cast(from), - static_cast(to)); + auto mapping_function = [](const void* source, void* target) { + map_owning_vtable_members(static_cast(source), + static_cast(target)); }; - return static_cast(get_or_create_vtable_erased( - from_vptr, &type_id_anchor, sizeof(ToVtable), mapper)); + return static_cast( + get_mapped_vtable(source_vtable_pointer, &conversion_anchor, + sizeof(ToVtable), mapping_function)); } template > diff --git a/scripts/protocol.j2 b/scripts/protocol.j2 index eb533ba..2f80db8 100644 --- a/scripts/protocol.j2 +++ b/scripts/protocol.j2 @@ -148,7 +148,7 @@ inline void map_owning_vtable_members(const From* from, typename protocol_owning to->xyz_protocol_clone = from->xyz_protocol_clone; to->xyz_protocol_move = from->xyz_protocol_move; to->xyz_protocol_destroy = from->xyz_protocol_destroy; - to->view_vt = get_or_create_mutable_vtable(from->view_vt); + to->view_vt = get_mutable_vtable(from->view_vt); {% for m in c.methods %} to->{{ m.name | mangle }}_{{ method_guids[loop.index0] }} = from->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; {% endfor %} @@ -284,13 +284,13 @@ class protocol<{{ full_class_name }}, Allocator> { : alloc_(other.alloc_) { if (alloc_ == other.alloc_) { p_ = std::exchange(other.p_, nullptr); - vtable_ = get_or_create_owning_vtable( + vtable_ = get_owning_vtable( std::exchange(other.vtable_, nullptr) ); } else { if (!other.valueless_after_move()) { p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_); - vtable_ = get_or_create_owning_vtable(other.vtable_); + vtable_ = get_owning_vtable(other.vtable_); } else { p_ = nullptr; vtable_ = nullptr; @@ -304,7 +304,7 @@ class protocol<{{ full_class_name }}, Allocator> { : alloc_(allocator_traits::select_on_container_copy_construction(other.alloc_)) { if (!other.valueless_after_move()) { p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); - vtable_ = get_or_create_owning_vtable(other.vtable_); + vtable_ = get_owning_vtable(other.vtable_); } else { p_ = nullptr; vtable_ = nullptr; @@ -318,7 +318,7 @@ class protocol<{{ full_class_name }}, Allocator> { : alloc_(alloc) { if (!other.valueless_after_move()) { p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); - vtable_ = get_or_create_owning_vtable(other.vtable_); + vtable_ = get_owning_vtable(other.vtable_); } else { p_ = nullptr; vtable_ = nullptr; @@ -546,13 +546,13 @@ class protocol_view { requires(!std::same_as) protocol_view(const protocol_view& other) : ptr_(other.ptr_), - vptr_(get_or_create_vtable(other.vptr_)) {} + vptr_(get_vtable(other.vptr_)) {} template requires(!std::same_as) protocol_view(const protocol_view& other) : ptr_(other.ptr_), - vptr_(get_or_create_vtable(&other.vptr_->const_view)) {} + vptr_(get_vtable(&other.vptr_->const_view)) {} {% for m in c.methods %}{% if m.is_const %} {% set params = [] %} @@ -600,7 +600,7 @@ class protocol_view<{{ full_class_name }}> { requires(!std::same_as) protocol_view(const protocol_view& other) : ptr_(other.ptr_), - vptr_(get_or_create_mutable_vtable(other.vptr_)) {} + vptr_(get_mutable_vtable(other.vptr_)) {} constexpr operator protocol_view() const noexcept { return protocol_view{static_cast(ptr_), &vptr_->const_view};