Skip to content

Commit f65f115

Browse files
authored
Add design section and function table
1 parent a95ff24 commit f65f115

1 file changed

Lines changed: 124 additions & 168 deletions

File tree

DRAFT.md

Lines changed: 124 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ D4148R0
66

77
Working Group: Library Evolution, Library
88

9-
Date: 2026-03-25
9+
Date: 2026-04-8
1010

1111
_Jonathan Coe \<<jonathanbcoe@gmail.com>\>_
1212

@@ -16,16 +16,30 @@ _Antony Peacock \<<ant.peacock@gmail.com>\>_
1616

1717
_Philip Craig \<<philip@pobox.com>\>_
1818

19+
_Neelofer Banglawala \<<dr.nbanglawala@gmail.com>\>_
20+
1921
## Abstract
2022

21-
We propose `protocol` and `protocol_view`, standard library vocabulary
23+
We propose `protocol<T, A>` and `protocol_view<T>`, standard library vocabulary
2224
types for structural subtyping in C++. Interfaces are specified as plain structs;
2325
any type whose member functions satisfy the interface is accepted without
24-
requiring explicit inheritance. The owning type, `protocol`, provides value
25-
semantics with deep-copy behaviour. The non-owning type, `protocol_view`,
26-
provides a lightweight reference to any conforming type, analogous to
27-
`std::span`. Both types are generated automatically by the compiler using static
28-
reflection, eliminating hand-written type-erasure boilerplate.
26+
requiring explicit inheritance.
27+
28+
Any type that provides member functions with the same names and function
29+
signatures as those specified by the interface is considered to be _conforming_
30+
to the protocol.
31+
32+
The owning type, `protocol`, provides value semantics (const-propagation and
33+
deep-copies) and support for custom allocators for any conforming type.
34+
35+
The non-owning type, `protocol_view`, provides a lightweight reference to any
36+
conforming type, analogous to `std::span`.
37+
38+
Ideally both `protocol` and `protocol_view` would be generated by the compiler
39+
using static reflection, eliminating hand-written type-erasure boilerplate or
40+
custom build steps. This proposal assumes the availability of static reflection
41+
and code injection and focuses solely on the design of the class templates
42+
`protocol` and `protocol_view`.
2943

3044
## History
3145

@@ -35,204 +49,140 @@ reflection, eliminating hand-written type-erasure boilerplate.
3549

3650
## Motivation
3751

38-
Traditional polymorphism in C++ requires inheritance from a common base class.
39-
This coupling prevents the retroactive application of interfaces to existing
40-
types and mandates reference semantics, which complicates ownership reasoning.
52+
Using `protocol`, functions can be written with run-time polymorphism
53+
(supporting mutliple types) without the types being related through inheritance.
54+
This is useful when types come from third-party libraries and cannot be readily
55+
altered to fit within a user-defined type heirarchy.
4156

42-
`std::function` provides structural subtyping for single callable objects, but
43-
no standardised equivalent exists for interfaces comprising multiple member
44-
functions. Developers must author bespoke type-erased wrappers, incurring
45-
substantial boilerplate for vtable construction, storage management, and
46-
const-propagation. Such wrappers are difficult to write correctly and must be
47-
duplicated for every interface.
57+
Currently, hand-written type-erasure or interface-specific solutions like
58+
`copyable_function` must be used for run-time polymorphism through structural
59+
subtyping.
4860

49-
Structural subtyping is well-established in other languages. PEP 544 introduced
50-
Protocols in Python: a class that structurally provides the required methods is
51-
considered a subtype without explicit inheritance. We propose an equivalent
52-
mechanism for C++.
61+
## Design
62+
63+
In C++26 we introduced `polymorphic<T>` which confers value-like semantics on a
64+
dynamically-allocated object. A `polymorphic<T>` may hold an object of a class
65+
publicly derived from `T`. In this proposal, we seek to further extend C++'s
66+
library of value-types with `protocol<T>` which can hold an object of any type
67+
so long as that type is a structural sub-type of `T`.
5368

54-
We propose `protocol<I>` to own an object of any type that satisfies interface
55-
`I`, providing value semantics and deep copy. We propose `protocol_view<I>` to
56-
refer non-owningly to any conforming type, enabling efficient observation at
57-
function boundaries without allocation or ownership transfer.
69+
Like `polymorphic`, `protocol` supports deep-copies, const propagation and
70+
custom allocators. Like `polymorphic`, `protocol` has a valueless state after
71+
after being moved from to allow move construction and move assignment without
72+
dynamic memory allocation.
5873

59-
Nominal subtyping allows non-owning polymorphism via raw pointers or references
60-
(`Base*`, `Base&`). Structural subtyping has no native equivalent. `protocol_view`
61-
fills this gap and is to `protocol` as `std::span` is to `std::vector`.
74+
Where `polymorphic<T>` is owning, `T*`, or `const T*` can be used as a
75+
non-owning reference type. There is no base class to take a pointer to for
76+
`protocol<T>` so we propose the addition of `protocol_view<T>` (and
77+
`protocol_view<const T>`) which are similar to `span` and `string_view` and give
78+
reference semantics to structural sub-types.
6279

6380
## Examples
6481

65-
The following examples illustrate the use of `protocol` for value-semantic
66-
ownership and `protocol_view` for non-owning observation. Note that `Circle`
67-
does not inherit from `Drawable`; it satisfies the interface structurally.
82+
### Minimal API examples
6883

69-
```cpp
70-
struct Drawable {
71-
std::string_view name() const;
72-
void draw();
73-
int draw_count() const;
74-
};
84+
TODO(jbcoe): Simple illustration of the API for `protocol` and `protocol_view`.
7585

76-
struct Circle {
77-
std::string_view name() const { return "Circle"; }
78-
void draw() { ++draw_count_; }
79-
int draw_count() const { return draw_count_; }
86+
### Function-like examples
8087

81-
private:
82-
int draw_count_ = 0;
88+
We can use `protocol` and `protocol_view` with appropriate
89+
structural types to implement and extend the standard library's
90+
existing set of function-objects:
91+
92+
```c++
93+
template <typename R, typename... Args>
94+
struct Function {
95+
// All special member functions are defaulted.
96+
R operator()(Args&&... args) const;
8397
};
8498
```
8599
86-
### `protocol` and value semantics
87-
88-
`protocol<I>` owns its contained object. Copying a `protocol` performs a
89-
deep copy of the underlying object.
100+
```c++
101+
template <typename R, typename... Args>
102+
struct MoveOnlyFunction {
103+
// Deleted copy constructor and copy assignment.
104+
MoveOnlyFunction(const MoveOnlyFunction&) = delete;
105+
MoveOnlyFunction& operator=(const MoveOnlyFunction&) = delete;
90106
91-
```cpp
92-
// Construct in-place
93-
protocol<Drawable> p1(std::in_place_type<Circle>);
107+
// Defaulted move constructor and move assignment.
108+
MoveOnlyFunction(MoveOnlyFunction&&) = default;
109+
MoveOnlyFunction& operator=(MoveOnlyFunction&&) = default;
94110
95-
// p2 is a deep copy of p1, including the underlying Circle object
96-
protocol<Drawable> p2 = p1;
97-
98-
p1.draw();
99-
p1.draw();
100-
101-
// p1 and p2 are independent
102-
assert(p1.draw_count() == 2);
103-
assert(p2.draw_count() == 0);
111+
R operator()(Args&&... args) const;
112+
};
104113
```
105114

106-
### `protocol_view` and reference semantics
107-
108-
`protocol_view<I>` is a non-owning view of any type satisfying interface
109-
`I`. Copying a `protocol_view` is a shallow operation; both copies refer to the
110-
same underlying object.
111-
112-
```cpp
113-
void print_name(protocol_view<const Drawable> view) {
114-
// A const view permits only const member functions.
115-
std::cout << "Name: " << view.name() << "\n";
116-
}
117-
118-
Circle circle;
119-
120-
// Bind a view to a concrete object without allocation or ownership transfer.
121-
print_name(circle);
122-
123-
protocol<Drawable> p(std::in_place_type<Circle>);
124-
125-
// Bind a view to an owning protocol object.
126-
print_name(p);
127-
128-
// Copying a view is shallow; both views refer to the same Circle.
129-
protocol_view<Drawable> v1(circle);
130-
protocol_view<Drawable> v2 = v1;
131-
v2.draw();
132-
assert(circle.draw_count() == 1);
115+
```c++
116+
template <typename R, typename... Args>
117+
struct MutatingFunction {
118+
// All special member functions are defaulted.
119+
R operator()(Args&&... args);
120+
};
133121
```
134122
135-
## Design
136-
137-
### Requirements
138-
139-
We require the following properties of `protocol` and `protocol_view`.
140-
141-
A type satisfies an interface based solely on the presence of member functions
142-
with conforming signatures; explicit inheritance is not required.
123+
```c++
124+
template <typename R, typename... Args>
125+
struct MoveOnlyMutatingFunction {
126+
// Deleted copy constructor and copy assignment.
127+
MoveOnlyFunction(const MoveOnlyFunction&) = delete;
128+
MoveOnlyFunction& operator=(const MoveOnlyFunction&) = delete;
143129
144-
`protocol<I>` provides value semantics. Copying a `protocol` object performs a
145-
deep copy of the underlying erased object. Moving a `protocol` object leaves it
146-
in a valid but unspecified (valueless) state, enabling efficient move operations
147-
without heap allocation.
130+
// Defaulted move constructor and move assignment.
131+
MoveOnlyFunction(MoveOnlyFunction&&) = default;
132+
MoveOnlyFunction& operator=(MoveOnlyFunction&&) = default;
148133
149-
`protocol_view<I>` provides non-owning reference semantics. It is constructible
150-
from any structurally conforming type, including `protocol<I>` itself. Method
151-
calls are forwarded through a synthesised vtable.
152-
153-
Const-correctness is strictly maintained. A `const`-qualified `protocol` object,
154-
or a `protocol_view<const I>`, permits the invocation of only `const`-qualified
155-
member functions of the underlying erased object.
156-
157-
The owning `protocol` is fully allocator-aware and properly supports
158-
`std::allocator_traits`.
159-
160-
The implementation of both types is generated automatically by the compiler
161-
using reflection, eliminating the need for manually authored boilerplate.
134+
R operator()(Args&&... args);
135+
};
136+
```
162137

163-
### Design decisions
138+
```c++
139+
struct OverloadedFunction {
140+
R1 operator()(Args1&&... args) const;
141+
R2 operator()(Args2&&... args);
142+
R3 operator()(Args3&&... args);
143+
};
144+
```
164145
165-
**Interface specification as a plain struct.** We chose to specify interfaces as
166-
unannotated structs rather than introducing a new declaration syntax. This
167-
decision favours minimal language impact and allows existing structs to serve as
168-
interface definitions without modification.
146+
There is currently no function-type in the standard library that can handle an
147+
overload set. The table below is illustrative of how flexible `protocol` and
148+
`protocol_view` are.
169149
170-
**Valueless-after-move state for `protocol`.** We require that a moved-from
171-
`protocol` enters a valueless state rather than retaining its previous value.
172-
This is consistent with `std::variant` and avoids the cost of constructing a
173-
sentinel object on move. Calling interface methods on a valueless `protocol`
174-
object is undefined behaviour.
150+
| Standard library type | Protocol equivalent |
151+
| :--- | :--- |
152+
| `std::copyable_function<R(Args...) const>` | `protocol<Function<R, Args...>>` |
153+
| `std::move_only_function<R(Args...) const>` | `protocol<MoveOnlyFunction<R, Args...>>` |
154+
| `std::function_ref<R(Args...) const>` | `protocol_view<Function<R, Args...>>` |
155+
| `std::copyable_function<R(Args...)>` | `protocol<MutatingFunction<R, Args...>>` |
156+
| `std::move_only_function<R(Args...)>` | `protocol<MoveOnlyMutatingFunction<R, Args...>>` |
157+
| `std::function_ref<R(Args...)>` | `protocol_view<MutatingFunction<R, Args...>>` |
158+
| ??? | `protocol<OverloadedFunction>` |
159+
| ??? | `protocol_view<OverloadedFunction>` |
175160
176-
**`protocol_view` does not own.** We considered whether `protocol_view` should
177-
support owning a small buffer optimisation. We rejected this in favour of a
178-
strict non-owning contract, consistent with `std::string_view` and `std::span`.
179-
Users requiring ownership should use `protocol`.
180161
181-
**Allocator awareness.** We require `protocol` to support `std::allocator_traits`
182-
for consistency with other owning standard library containers. `protocol_view`
183-
requires no allocation and is therefore not allocator-aware.
162+
### Comparison with proxy
184163
185164
## Impact on the Standard
186165
187-
This is a library proposal that fundamentally depends on core-language reflection
188-
facilities capable of programmatic class generation.
189-
190-
C++26 reflection (P2996) provides the introspection capabilities required to
191-
validate structural conformity at compile time. However, synthesising a protocol
192-
wrapper requires the ability to inject member functions into a class
193-
definition—a generative capability not present in C++26. This proposal therefore
194-
serves as a motivating use case for future extensions to the language's
195-
reflection and code-injection facilities.
166+
This proposal is a library extension. It requires language support for code
167+
injection from static reflection and the addition of a new standard library
168+
header `<protocol>`."
196169
197170
## Technical Specifications
198171
199-
The synthesis of `protocol` and `protocol_view` relies on static reflection.
200-
201-
### Elements implementable with C++26 reflection
202-
203-
C++26 reflection (P2996) provides sufficient introspection for structural
204-
validation. Using the reflection operator (`^`) and the `std::meta` namespace, a
205-
metaprogram can extract the names, signatures, and `const`-qualifiers of the
206-
member functions declared in an interface struct. It can then verify that a
207-
candidate type provides matching member functions, replacing manually authored
208-
concepts with a generic, reflection-driven structural constraint.
172+
TODO(jbcoe): Adopt relevant wording from value-types, add wording for reflected
173+
methods.
209174
210-
### Elements requiring post-C++26 additions
175+
## Polls
211176
212-
C++26 reflection does not yet support generalised code injection. The following
213-
elements of the implementation require future standard additions.
214-
215-
The dispatch mechanism—whether a virtual hierarchy or a synthesised vtable—
216-
requires injection of virtual functions or function pointers derived from an
217-
introspected interface. `std::meta::define_class` is currently restricted to
218-
non-static data members and does not support member function injection.
219-
220-
Generating the forwarding member functions of the `protocol` wrapper likewise
221-
requires member function injection. Expanding introspected function parameters
222-
into parameter packs for forwarding requires range-splicing of function arguments
223-
and return types, which depends on more advanced injection proposals beyond P2996.
224-
225-
Until such injection facilities are standardised, a practical implementation
226-
requires external tooling for code generation, with C++26 reflection handling
227-
structural validation.
177+
- Should we work to standardize `protocol` and `protocol_view`?
228178
229179
## Reference Implementation
230180
231-
A reference implementation using an AST-based Python code generator
232-
(`py_cppmodel`) to simulate post-C++26 code injection is available at
233-
[py_cppmodel]. The implementation demonstrates the feasibility of vtable
234-
generation, allocator awareness, and the value semantics properties required by
235-
this proposal.
181+
A reference implementation, using an AST-based Python code generator to simulate
182+
post-C++26 code injection, is available at
183+
<https://github.com/jbcoe/cc-protocol>. The implementation demonstrates the
184+
feasibility of vtable generation, allocator awareness, and the value semantics
185+
properties required by this proposal.
236186
237187
## Acknowledgements
238188
@@ -246,5 +196,11 @@ this proposal.
246196
247197
[P2996] _Reflection for C++26_. <https://isocpp.org/files/papers/P2996R13.html>
248198
199+
[Metaclasses for generative C++]
200+
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p0707r5.pdf>
201+
202+
[P3086R4 Proxy]
203+
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3086r5.html>
204+
249205
[py_cppmodel] _Python wrappers for clang's parsing of C++ to simplify AST
250206
analysis_. <https://github.com/jbcoe/py_cppmodel>

0 commit comments

Comments
 (0)