An experimental C++ library and code generation tool for static structural subtyping (duck typing) with value semantics.
Polymorphic interfaces in C++ traditionally require explicit inheritance from an
abstract base class. This nominal subtyping tightly couples independent
components, makes it impossible to retroactively apply interfaces to third-party
types, and typically forces reference semantics (e.g., std::unique_ptr).
Inspired by Python's Protocol (PEP 544), this repository explores bringing a
similar paradigm to C++. By using AST parsing (via Clang) and code generation,
the tool synthesizes type-erased wrappers that accept any type structurally
conforming to an interface, without inheritance.
These protocols maintain deep-copy value semantics, strict const-propagation,
and allocator awareness, consistent with the design of jbcoe/value_types
(P3019).
As C++ reflection (P2996) matures and code injection is added in future
standards (C++29+), the generation approach demonstrated here via py_cppmodel
will be achievable natively within the language.
A draft proposal is available in DRAFT.md.
The interface is a plain struct with no virtual keywords, no = 0, and no base
classes.
#pragma once
#include <string>
#include <vector>
namespace xyz {
struct B {
void process(const std::string& input);
std::vector<int> get_results() const;
bool is_ready() const;
};
} // namespace xyzWrite your concrete type. It does not need to inherit from xyz::B; it only
needs to structurally provide the methods defined in the interface.
namespace xyz {
class MyImplementation {
std::vector<int> results_;
bool ready_ = false;
public:
// Structurally matches xyz::B
void process(const std::string& input) {
results_.push_back(input.length());
ready_ = true;
}
std::vector<int> get_results() const { return results_; }
bool is_ready() const { return ready_; }
};
} // namespace xyzxyz::protocol<xyz::B> is an automatically generated type-erased wrapper. It
copies deeply, propagates const correctly, and supports custom allocators.
#include "generated/protocol_B.h"
void run_pipeline(xyz::protocol<xyz::B> worker) {
if (!worker.is_ready()) {
worker.process("hello protocols");
}
for (int result : worker.get_results()) {
// ...
}
}
int main() {
// Construct the protocol in-place with our implementation
xyz::protocol<xyz::B> p(std::in_place_type<xyz::MyImplementation>);
run_pipeline(p); // Pass by value!
return 0;
}The generated wrapper uses C++20 concepts and requires clauses: any structural
mismatch produces clear, pinpointed compile-time errors rather than deeply nested
template instantiation failures.
class BadImplementation {
public:
void process(const std::string& input);
// ERROR: Missing get_results()
// ERROR: is_ready() is missing 'const'
bool is_ready();
};
// COMPILER ERROR:
// constraints not satisfied
// the required expression 'std::as_const(t).is_ready()' is invalidAlongside protocol, the code generator also produces a protocol_view
specialization. While protocol manages the lifecycle of the underlying object
(with deep-copy value semantics), protocol_view is a lightweight, non-owning
reference, analogous to std::string_view or std::span, but for protocols.
// `view` observes the object without owning or copying it.
void inspect(xyz::protocol_view<xyz::B> view) {
if (view.is_ready()) {
// ...
}
}
int main() {
xyz::MyImplementation impl;
// Implicitly constructs a view over `impl` since it fulfills the structural requirements.
inspect(impl);
xyz::protocol<xyz::B> p(std::in_place_type<xyz::MyImplementation>);
// Implicitly constructs a view over `p` as the protocol itself satisfies the requirements.
inspect(p);
return 0;
}protocol_view provides non-owning, allocation-free structural dispatch at
function boundaries, avoiding deep copies while dispatching through a
lightweight indirection.
The code generator supports two dispatch strategies:
-
Virtual Dispatch (Default): Generates a traditional C++ polymorphic class hierarchy with
virtualmethods. The type-erased wrapper heap-allocates a control block derived from a common interface. -
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.
# Build and run the benchmark comparing the two implementations
./scripts/cmake.sh benchmarkFor build instructions, testing, contributing guidelines, and a deeper look into the code generation architecture, see the Developer Guide.
-
PEP 544: Protocols: Structural subtyping (static duck typing)
-
P2996: Reflection for C++26
-
py_cppmodel: Python wrappers for clang's parsing of C++