An experimental C++ library and generation tool for enabling 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 utilizing AST parsing (via Clang) and code
generation, we can automatically synthesize type-erased wrappers that accept any
type structurally conforming to an interface.
Crucially, these protocols maintain deep-copy value semantics, strict
const-propagation, and allocator awareness, aligning heavily with the
principles of jbcoe/value_types (P3019).
As C++ reflection (P2996) matures and advanced code injection capabilities are
added in future standards (C++29+), the generation process demonstrated here via
py_cppmodel will be achievable natively within the language.
A draft proposal detailing this feature can be found in proposals/DRAFT.md.
Unlike traditional polymorphism, the interface is just a struct. 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 xyzWe can now use xyz::protocol<xyz::B>, 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 emits 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 invalidFor instructions on how to build, test, and contribute to this project, as well as a deeper look into the code generation architecture, please refer to 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++