diff --git a/DRAFT.md b/DRAFT.md index f2a1802..e394864 100644 --- a/DRAFT.md +++ b/DRAFT.md @@ -6,7 +6,7 @@ D4148R0 Working Group: Library Evolution, Library -Date: 2026-03-25 +Date: 2026-04-8 _Jonathan Coe \<\>_ @@ -16,16 +16,30 @@ _Antony Peacock \<\>_ _Philip Craig \<\>_ +_Neelofer Banglawala \<\>_ + ## Abstract -We propose `protocol` and `protocol_view`, standard library vocabulary +We propose `protocol` and `protocol_view`, standard library vocabulary types for structural subtyping in C++. Interfaces are specified as plain structs; any type whose member functions satisfy the interface is accepted without -requiring explicit inheritance. The owning type, `protocol`, provides value -semantics with deep-copy behaviour. The non-owning type, `protocol_view`, -provides a lightweight reference to any conforming type, analogous to -`std::span`. Both types are generated automatically by the compiler using static -reflection, eliminating hand-written type-erasure boilerplate. +requiring explicit inheritance. + +Any type that provides member functions with the same names and function +signatures as those specified by the interface is considered to be _conforming_ +to the protocol. + +The owning type, `protocol`, provides value semantics (const-propagation and +deep-copies) and support for custom allocators for any conforming type. + +The non-owning type, `protocol_view`, provides a lightweight reference to any +conforming type, analogous to `std::span`. + +Ideally both `protocol` and `protocol_view` would be generated by the compiler +using static reflection, eliminating hand-written type-erasure boilerplate or +custom build steps. This proposal assumes the availability of static reflection +and code injection and focuses solely on the design of the class templates +`protocol` and `protocol_view`. ## History @@ -35,204 +49,140 @@ reflection, eliminating hand-written type-erasure boilerplate. ## Motivation -Traditional polymorphism in C++ requires inheritance from a common base class. -This coupling prevents the retroactive application of interfaces to existing -types and mandates reference semantics, which complicates ownership reasoning. +Using `protocol`, functions can be written with run-time polymorphism +(supporting mutliple types) without the types being related through inheritance. +This is useful when types come from third-party libraries and cannot be readily +altered to fit within a user-defined type heirarchy. -`std::function` provides structural subtyping for single callable objects, but -no standardised equivalent exists for interfaces comprising multiple member -functions. Developers must author bespoke type-erased wrappers, incurring -substantial boilerplate for vtable construction, storage management, and -const-propagation. Such wrappers are difficult to write correctly and must be -duplicated for every interface. +Currently, hand-written type-erasure or interface-specific solutions like +`copyable_function` must be used for run-time polymorphism through structural +subtyping. -Structural subtyping is well-established in other languages. PEP 544 introduced -Protocols in Python: a class that structurally provides the required methods is -considered a subtype without explicit inheritance. We propose an equivalent -mechanism for C++. +## Design + +In C++26 we introduced `polymorphic` which confers value-like semantics on a +dynamically-allocated object. A `polymorphic` may hold an object of a class +publicly derived from `T`. In this proposal, we seek to further extend C++'s +library of value-types with `protocol` which can hold an object of any type +so long as that type is a structural sub-type of `T`. -We propose `protocol` to own an object of any type that satisfies interface -`I`, providing value semantics and deep copy. We propose `protocol_view` to -refer non-owningly to any conforming type, enabling efficient observation at -function boundaries without allocation or ownership transfer. +Like `polymorphic`, `protocol` supports deep-copies, const propagation and +custom allocators. Like `polymorphic`, `protocol` has a valueless state after +after being moved from to allow move construction and move assignment without +dynamic memory allocation. -Nominal subtyping allows non-owning polymorphism via raw pointers or references -(`Base*`, `Base&`). Structural subtyping has no native equivalent. `protocol_view` -fills this gap and is to `protocol` as `std::span` is to `std::vector`. +Where `polymorphic` is owning, `T*`, or `const T*` can be used as a +non-owning reference type. There is no base class to take a pointer to for +`protocol` so we propose the addition of `protocol_view` (and +`protocol_view`) which are similar to `span` and `string_view` and give +reference semantics to structural sub-types. ## Examples -The following examples illustrate the use of `protocol` for value-semantic -ownership and `protocol_view` for non-owning observation. Note that `Circle` -does not inherit from `Drawable`; it satisfies the interface structurally. +### Minimal API examples -```cpp -struct Drawable { - std::string_view name() const; - void draw(); - int draw_count() const; -}; +TODO(jbcoe): Simple illustration of the API for `protocol` and `protocol_view`. -struct Circle { - std::string_view name() const { return "Circle"; } - void draw() { ++draw_count_; } - int draw_count() const { return draw_count_; } +### Function-like examples -private: - int draw_count_ = 0; +We can use `protocol` and `protocol_view` with appropriate +structural types to implement and extend the standard library's +existing set of function-objects: + +```c++ +template +struct Function { + // All special member functions are defaulted. + R operator()(Args&&... args) const; }; ``` -### `protocol` and value semantics - -`protocol` owns its contained object. Copying a `protocol` performs a -deep copy of the underlying object. +```c++ +template +struct MoveOnlyFunction { + // Deleted copy constructor and copy assignment. + MoveOnlyFunction(const MoveOnlyFunction&) = delete; + MoveOnlyFunction& operator=(const MoveOnlyFunction&) = delete; -```cpp -// Construct in-place -protocol p1(std::in_place_type); + // Defaulted move constructor and move assignment. + MoveOnlyFunction(MoveOnlyFunction&&) = default; + MoveOnlyFunction& operator=(MoveOnlyFunction&&) = default; -// p2 is a deep copy of p1, including the underlying Circle object -protocol p2 = p1; - -p1.draw(); -p1.draw(); - -// p1 and p2 are independent -assert(p1.draw_count() == 2); -assert(p2.draw_count() == 0); + R operator()(Args&&... args) const; +}; ``` -### `protocol_view` and reference semantics - -`protocol_view` is a non-owning view of any type satisfying interface -`I`. Copying a `protocol_view` is a shallow operation; both copies refer to the -same underlying object. - -```cpp -void print_name(protocol_view view) { - // A const view permits only const member functions. - std::cout << "Name: " << view.name() << "\n"; -} - -Circle circle; - -// Bind a view to a concrete object without allocation or ownership transfer. -print_name(circle); - -protocol p(std::in_place_type); - -// Bind a view to an owning protocol object. -print_name(p); - -// Copying a view is shallow; both views refer to the same Circle. -protocol_view v1(circle); -protocol_view v2 = v1; -v2.draw(); -assert(circle.draw_count() == 1); +```c++ +template +struct MutatingFunction { + // All special member functions are defaulted. + R operator()(Args&&... args); +}; ``` -## Design - -### Requirements - -We require the following properties of `protocol` and `protocol_view`. - -A type satisfies an interface based solely on the presence of member functions -with conforming signatures; explicit inheritance is not required. +```c++ +template +struct MoveOnlyMutatingFunction { + // Deleted copy constructor and copy assignment. + MoveOnlyFunction(const MoveOnlyFunction&) = delete; + MoveOnlyFunction& operator=(const MoveOnlyFunction&) = delete; -`protocol` provides value semantics. Copying a `protocol` object performs a -deep copy of the underlying erased object. Moving a `protocol` object leaves it -in a valid but unspecified (valueless) state, enabling efficient move operations -without heap allocation. + // Defaulted move constructor and move assignment. + MoveOnlyFunction(MoveOnlyFunction&&) = default; + MoveOnlyFunction& operator=(MoveOnlyFunction&&) = default; -`protocol_view` provides non-owning reference semantics. It is constructible -from any structurally conforming type, including `protocol` itself. Method -calls are forwarded through a synthesised vtable. - -Const-correctness is strictly maintained. A `const`-qualified `protocol` object, -or a `protocol_view`, permits the invocation of only `const`-qualified -member functions of the underlying erased object. - -The owning `protocol` is fully allocator-aware and properly supports -`std::allocator_traits`. - -The implementation of both types is generated automatically by the compiler -using reflection, eliminating the need for manually authored boilerplate. + R operator()(Args&&... args); +}; +``` -### Design decisions +```c++ +struct OverloadedFunction { + R1 operator()(Args1&&... args) const; + R2 operator()(Args2&&... args); + R3 operator()(Args3&&... args); +}; +``` -**Interface specification as a plain struct.** We chose to specify interfaces as -unannotated structs rather than introducing a new declaration syntax. This -decision favours minimal language impact and allows existing structs to serve as -interface definitions without modification. +There is currently no function-type in the standard library that can handle an +overload set. The table below is illustrative of how flexible `protocol` and +`protocol_view` are. -**Valueless-after-move state for `protocol`.** We require that a moved-from -`protocol` enters a valueless state rather than retaining its previous value. -This is consistent with `std::variant` and avoids the cost of constructing a -sentinel object on move. Calling interface methods on a valueless `protocol` -object is undefined behaviour. +| Standard library type | Protocol equivalent | +| :--- | :--- | +| `std::copyable_function` | `protocol>` | +| `std::move_only_function` | `protocol>` | +| `std::function_ref` | `protocol_view>` | +| `std::copyable_function` | `protocol>` | +| `std::move_only_function` | `protocol>` | +| `std::function_ref` | `protocol_view>` | +| ??? | `protocol` | +| ??? | `protocol_view` | -**`protocol_view` does not own.** We considered whether `protocol_view` should -support owning a small buffer optimisation. We rejected this in favour of a -strict non-owning contract, consistent with `std::string_view` and `std::span`. -Users requiring ownership should use `protocol`. -**Allocator awareness.** We require `protocol` to support `std::allocator_traits` -for consistency with other owning standard library containers. `protocol_view` -requires no allocation and is therefore not allocator-aware. +### Comparison with proxy ## Impact on the Standard -This is a library proposal that fundamentally depends on core-language reflection -facilities capable of programmatic class generation. - -C++26 reflection (P2996) provides the introspection capabilities required to -validate structural conformity at compile time. However, synthesising a protocol -wrapper requires the ability to inject member functions into a class -definition—a generative capability not present in C++26. This proposal therefore -serves as a motivating use case for future extensions to the language's -reflection and code-injection facilities. +This proposal is a library extension. It requires language support for code +injection from static reflection and the addition of a new standard library +header ``." ## Technical Specifications -The synthesis of `protocol` and `protocol_view` relies on static reflection. - -### Elements implementable with C++26 reflection - -C++26 reflection (P2996) provides sufficient introspection for structural -validation. Using the reflection operator (`^`) and the `std::meta` namespace, a -metaprogram can extract the names, signatures, and `const`-qualifiers of the -member functions declared in an interface struct. It can then verify that a -candidate type provides matching member functions, replacing manually authored -concepts with a generic, reflection-driven structural constraint. +TODO(jbcoe): Adopt relevant wording from value-types, add wording for reflected +methods. -### Elements requiring post-C++26 additions +## Polls -C++26 reflection does not yet support generalised code injection. The following -elements of the implementation require future standard additions. - -The dispatch mechanism—whether a virtual hierarchy or a synthesised vtable— -requires injection of virtual functions or function pointers derived from an -introspected interface. `std::meta::define_class` is currently restricted to -non-static data members and does not support member function injection. - -Generating the forwarding member functions of the `protocol` wrapper likewise -requires member function injection. Expanding introspected function parameters -into parameter packs for forwarding requires range-splicing of function arguments -and return types, which depends on more advanced injection proposals beyond P2996. - -Until such injection facilities are standardised, a practical implementation -requires external tooling for code generation, with C++26 reflection handling -structural validation. +- Should we work to standardize `protocol` and `protocol_view`? ## Reference Implementation -A reference implementation using an AST-based Python code generator -(`py_cppmodel`) to simulate post-C++26 code injection is available at -[py_cppmodel]. The implementation demonstrates the feasibility of vtable -generation, allocator awareness, and the value semantics properties required by -this proposal. +A reference implementation, using an AST-based Python code generator to simulate +post-C++26 code injection, is available at +. The implementation demonstrates the +feasibility of vtable generation, allocator awareness, and the value semantics +properties required by this proposal. ## Acknowledgements @@ -246,5 +196,11 @@ this proposal. [P2996] _Reflection for C++26_. +[Metaclasses for generative C++] + + +[P3086R4 Proxy] + + [py_cppmodel] _Python wrappers for clang's parsing of C++ to simplify AST analysis_.