Skip to content

Compare protocol with proxy #26

@jbcoe

Description

@jbcoe

There is another proposal in similar design space, https://github.com/ngcpp/proxy.

We should understand and compare similarities and differences.

AI-comparison:


Protocol vs. Proxy: A Comparison of Structural Subtyping in C++

This document outlines the fundamental differences between two prominent approaches to type-erased structural subtyping in C++: the protocol pattern (inspired by P3019 std::polymorphic and PEP 544, implemented as xyz::protocol) and the proxy pattern (proposed in P3086, implemented in ngcpp/proxy).

Fundamental Differences

1. Interface Definition (Reflection vs. Library Boilerplate)

  • Protocol: Employs an unobtrusive approach. Interfaces are defined as standard C++ structs containing pure declarations. The library relies on (simulated or future C++26) static reflection to introspect the struct and generate a type-erased vtable wrapper.
  • Proxy: Employs a library-centric approach. Interfaces (called "Facades") are built using library templates (pro::facade_builder) and macros (PRO_DEF_MEMBER_DISPATCH). The facade explicitly dictates the dispatch mechanism, conventions, and constraints.

2. Interaction Semantics (Value vs. Pointer)

  • Protocol: Models true value semantics (Direct Semantics). You interact with a protocol<T> exactly as you would a concrete value, using the dot (.) operator. It behaves like a polymorphic value container.
  • Proxy: Models pointer semantics. You interact with a pro::proxy<F> as if it were a smart pointer, using the arrow (->) operator or passing the proxy to dispatch functors. This choice deliberately avoids name collisions between the wrapper's utility methods and the erased type's methods.

3. Configurability and "Meta-Properties"

  • Protocol: Operates as a fixed container. Memory layout, copyability, and move behavior are determined by the protocol template implementation and standard C++ mechanisms (e.g., allocators).
  • Proxy: The Facade itself describes the "meta-properties" of the container. A facade can specify whether the type is copyable, trivially relocatable (for memmove optimizations), and even restrict the small-buffer optimization (SBO) size or pointer layout.

4. Subtyping and Substitution

  • Protocol: Distinct generated types. protocol<A> and protocol<B> are unrelated C++ classes, even if B is a superset of A.
  • Proxy: Supports implicit downgrading. If a RichFacade includes a LeanFacade via basic_facade_builder::add_facade, a proxy<RichFacade> can be transparently substituted where a proxy<LeanFacade> is expected.

Pattern Comparison Table

Pattern protocol (cc-protocol / P3019 style) proxy (ngcpp/proxy / P3086 style)
Defining an Interface struct Draw {
  void draw() const;
};
PRO_DEF_MEMBER_DISPATCH(MemDraw, draw);
struct DrawFacade : pro::facade_builder
  ::add_convention<MemDraw, void() const>
  ::build {};
Instantiation (Owning) xyz::protocol<Draw> p = Circle{}; pro::proxy<DrawFacade> p = pro::make_proxy<DrawFacade>(Circle{});
Method Invocation p.draw(); p->draw(); (pointer semantics)
Non-Owning View void render(xyz::protocol_view<Draw> v); void render(pro::proxy_view<DrawFacade> v);
Reassignment p = Square{}; p = pro::make_proxy<DrawFacade>(Square{});

Patterns Exclusive to One Library

Implementable ONLY in proxy

1. Facade Downgrading / Substitution

  • Pattern: Passing a proxy<DrawableAndPrintable> to a function expecting proxy<Drawable>.
  • Missing Feature in protocol: protocol generates entirely distinct, isolated classes for each interface. Because it lacks a relational composition mechanism like add_facade, the compiler has no way to map the vtable of a superset protocol to a subset protocol without a heavy, manual re-allocation/re-wrapping process.

2. Interface-Defined Layout and Relocatability

  • Pattern: Forcing the polymorphic wrapper to be strictly 16 bytes (pointer + vtable) with no SBO, or declaring that the type can be optimally relocated via memcpy to save cycles during vector reallocations.
  • Missing Feature in protocol: protocol uses a "one-size-fits-all" container logic (like std::function or std::any). It cannot bake memory constraints or relocation semantics into the structural interface itself, relying instead on standard C++ move constructors and std::allocator.

3. Weak Handles

  • Pattern: Creating a pro::weak_proxy that observes a polymorphic object without extending its lifetime, analogous to std::weak_ptr.
  • Missing Feature in protocol: protocol currently only implements strict owning (protocol) and non-owning observation (protocol_view). It does not inherently support shared ownership semantics with weak referencing built-in.

Implementable ONLY in protocol

1. True Value Semantics ("Direct" Semantics)

  • Pattern: Treating the polymorphic object literally like a value: obj.do_something().
  • Missing Feature in proxy: proxy explicitly separates the container from the object, enforcing pointer semantics (obj->do_something()). It lacks the generative injection necessary to synthesize member functions directly onto the wrapper that perfectly mirror the erased type.

2. Unobtrusive Interface Declarations

  • Pattern: Using standard, unmodified C++ structs (or existing 3rd-party structs) as the interface definition.
  • Missing Feature in proxy: proxy requires intrusive macro usage (PRO_DEF_MEMBER_DISPATCH) and specific library builder templates to create a Facade. It lacks the reflection capabilities required to automatically introspect a standard C++ struct and infer the dispatch conventions.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions