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>namespacexyz {
structB {
voidprocess(const std::string& input);
std::vector<int> get_results() const;
boolis_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.
namespacexyz {
classMyImplementation {
std::vector<int> results_;
bool ready_ = false;
public:// Structurally matches xyz::Bvoidprocess(const std::string& input) {
results_.push_back(input.length());
ready_ = true;
}
std::vector<int> get_results() const { return results_; }
boolis_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"voidrun_pipeline(xyz::protocol<xyz::B> worker) {
if (!worker.is_ready()) {
worker.process("hello protocols");
}
for (int result : worker.get_results()) {
// ...
}
}
intmain() {
// 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!return0;
}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.
classBadImplementation {
public:voidprocess(const std::string& input);
// ERROR: Missing get_results()// ERROR: is_ready() is missing 'const'boolis_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.voidinspect(xyz::protocol_view<xyz::B> view) {
if (view.is_ready()) {
// ...
}
}
intmain() {
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);
return0;
}protocol_view provides non-owning, allocation-free structural dispatch at
function boundaries, avoiding deep copies while dispatching through a
lightweight indirection.
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.
The library provides a protocol_benchmark target for measuring the performance of the protocol across allocations, copies, moves, and member function calls.
# Build and run the benchmark
./scripts/cmake.sh benchmarkFor build instructions, testing, contributing guidelines, and a deeper look into the code generation architecture, see the Developer Guide.