The isocpp.org piece by Richard Hickling on C++26 and Python bindings for algorithmic trading frames the feature as a way to eliminate the manual bridge code between C++ execution engines and Python strategy layers. That framing is accurate as far as it goes. P2996, voted into the C++26 working draft at the WG21 Wrocław meeting in November 2024, allows a generic template to inspect a C++ class at compile time, read its public member names and types, and emit the corresponding pybind11 registration calls without manual authorship. The binding becomes a derived artifact of the header file rather than a separately maintained shadow copy.
Boris Staletić’s month-long experiment using Bloomberg’s experimental Clang fork found roughly 70 to 80 percent automation coverage for typical C++ classes, with three categories consistently falling outside what reflection can handle: overloaded methods, default argument values, and pointer ownership semantics.
Each of these failures maps to a specific API design choice. The information needed was never encoded in the type system; P2996 can only surface what is already there. Changing that encoding, before P2996 ships in production toolchains, is the most concrete preparation you can make today.
Overloads: name things distinctly
C++ allows multiple methods to share a name if their parameter types differ:
class ImpliedVolSolver {
public:
double solve(double spot, double strike, double rate, double expiry);
double solve(double spot, double strike, double rate, double expiry, double dividend);
double solve(const MarketData& data);
};
A reflection-based generator sees all three solve methods when iterating std::meta::members_of(^ImpliedVolSolver). Constructing a disambiguation using std::meta::parameters_of() is mechanically possible. The question it cannot answer is which Python-facing name to assign: one solve with runtime dispatch, three distinct names, or some combination? That is an API design decision about how the Python consumer should experience the class, and static metadata carries no opinion about it.
The reflection-compatible design names them distinctly:
class ImpliedVolSolver {
public:
double solve(double spot, double strike, double rate, double expiry);
double solve_with_dividend(double spot, double strike, double rate, double expiry, double dividend);
double solve_from_market(const MarketData& data);
};
The generator sees three distinct identifiers and emits three .def() calls without disambiguation logic. The Python API is more readable: solve_from_market(data) communicates its intent in a way that solve(data) does not, particularly when the scalar variant is also in scope.
The standard objection is that C++ overloading is natural and requiring descriptive names to satisfy a generator inverts design priorities. The counter is that Python has no overloading; any Python consumer of this class needs to understand which variant applies, and a descriptive name makes that explicit rather than requiring inspection of the type signature. The generator’s preference and the Python consumer’s readability align here.
Default arguments: use configuration types
P2996 exposes std::meta::has_default_argument(param) to detect that a parameter has a default, but not the value itself. A generator can tell pybind11 that a parameter exists, but cannot supply the default value that makes it optional from Python:
// Generator produces py::arg("rate") but not py::arg("rate") = 0.05.
// Python consumers must supply all arguments; the defaults are invisible.
double price(double spot, double strike, double vol,
double rate = 0.05, double expiry = 1.0);
The omission is deliberate in P2996’s design: default argument expressions can be arbitrary computations, and making their values accessible through std::meta::info would require standardizing the representation of compile-time expressions. That problem is deferred. For binding generation, the result is that parameter defaults are invisible to reflection.
The alternative is a configuration struct:
struct PricingParams {
double rate = 0.05;
double expiry = 1.0;
bool use_continuous_compounding = true;
};
double price(double spot, double strike, double vol, PricingParams params = {});
A reflection-based generator binds PricingParams as a Python class with def_readwrite for each field. The defaults live in the C++ definition, visible to both Python callers and the reflection generator. Python code reads p = PricingParams(); p.rate = 0.07; pricer.price(100, 105, 0.2, p) rather than a positional argument list, which is explicit about what is being overridden.
Configuration structs have merit beyond binding compatibility. Adding a new parameter with a default does not break existing callers, because PricingParams can acquire a new field without changing the price signature. The struct is also serializable and loggable, which is useful for debugging and audit trails in trading systems where reproducing a specific pricing run matters.
Ownership: encode it in the type
pybind11 has six distinct return value policies because raw pointer return types carry no ownership information. const Config* and Config* look identical to a reflection-based generator, which must either apply a uniform default (usually reference, meaning C++ retains ownership) or require annotation. Getting this wrong for an owning return causes Python to never free the object; getting it wrong for a non-owning return causes Python to free something C++ still holds a reference to.
The C++ type system can express ownership explicitly:
class Engine {
public:
// Raw pointers: ownership unclear, generator must guess
const Config* get_config() const;
Settings* create_settings();
// Ownership encoded in the type
const Config& get_config() const; // borrowed, lifetime tied to Engine
std::unique_ptr<Settings> create_settings(); // caller owns, no ambiguity
};
const Config& maps to reference_internal by standard pybind11 convention. std::unique_ptr<Settings> maps to take_ownership. A generator that handles these two return types correctly covers the ownership semantics without separate annotation, because the type encodes the policy.
This is not always achievable. Some APIs return raw pointers for performance, or because the ownership model is more complex than what unique_ptr captures. For those cases, P1854, a companion proposal for user-defined attributes inspectable at compile time, provides a path:
[[py::return_policy(reference_internal)]]
const Config* get_config() const;
P1854 is not in the C++26 draft. Until it lands, encoding ownership through type choice is what a reflection-based generator can act on.
The structure common to all three gaps
Overloads, default values, and ownership semantics share a property: C++ permits the relevant information to remain implicit. Multiple methods can share a name because the compiler resolves them by argument type. Default values live in the AST without being visible to callers as values. Pointer return types make no statement about who owns the pointed-to object.
None of these are mistakes in C++. The problem surfaces at the binding layer, where Python consumers need explicit information that C++ allows to stay implicit, and where P2996 can only automate what the type system already encodes.
The APIs that maximize reflection automation are APIs where the Python consumer’s information needs drove the design: distinct method names that communicate intent, parameter counts kept manageable without hidden defaults, return types that carry ownership in their type rather than their documentation. These are also attributes of a clean C++ interface for C++ consumers. The design patterns that help a binding generator help a human reader for the same underlying reason: both need to understand the contract without reading the source.
External generators like Binder, which Rosetta uses to manage its tens of thousands of lines of pybind11 bindings, handle the hard cases through .binder configuration files: explicit annotations in a separate file that tell the generator how to handle overloads and ownership. The configuration file is a third representation of the API, alongside the C++ declaration and the generated binding. P1854 inline attributes are the in-language equivalent, colocated with the declaration and checked by the compiler. Until P1854 ships, the choice is between external configuration and designing the API to not need it.
Practical timeline
Bloomberg’s clang-p2996 fork runs on Compiler Explorer (select “Clang experimental P2996”) and is sufficient for testing whether a given class generates clean bindings. Upstream Clang integration was in progress as of early 2026; MSVC has no announced timeline. Production toolchain availability for most shops looks like 2027 at the earliest, with conservative estimates pushing to 2028 for MSVC-dependent codebases.
Starting with these design patterns now costs nothing and has independent value. A C++ pricer written today with distinct method names, configuration structs for parameter sets, and unique_ptr returns for owned objects will bind cleanly with P2996 when it ships. The same pricer written with accumulated overloads, implicit ownership contracts, and long parameter lists with defaults will need configuration files or manual registration for the hard cases, whether the generator lives inside the compiler or outside it.
The ceiling on P2996 binding automation is well-defined. The cases that breach it are identifiable at code review time, and the API patterns that avoid them are worth applying regardless of when the feature becomes available.