Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 124 additions & 168 deletions DRAFT.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ D4148R0

Working Group: Library Evolution, Library

Date: 2026-03-25
Date: 2026-04-8

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

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

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

_Neelofer Banglawala \<<dr.nbanglawala@gmail.com>\>_

## Abstract

We propose `protocol` and `protocol_view`, standard library vocabulary
We propose `protocol<T, A>` and `protocol_view<T>`, 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

Expand All @@ -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<T>` which confers value-like semantics on a
dynamically-allocated object. A `polymorphic<T>` 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<T>` which can hold an object of any type
so long as that type is a structural sub-type of `T`.

We propose `protocol<I>` to own an object of any type that satisfies interface
`I`, providing value semantics and deep copy. We propose `protocol_view<I>` 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<T>` 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<T>` so we propose the addition of `protocol_view<T>` (and
`protocol_view<const T>`) 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 <typename R, typename... Args>
struct Function {
// All special member functions are defaulted.
R operator()(Args&&... args) const;
};
```

### `protocol` and value semantics

`protocol<I>` owns its contained object. Copying a `protocol` performs a
deep copy of the underlying object.
```c++
template <typename R, typename... Args>
struct MoveOnlyFunction {
// Deleted copy constructor and copy assignment.
MoveOnlyFunction(const MoveOnlyFunction&) = delete;
MoveOnlyFunction& operator=(const MoveOnlyFunction&) = delete;

```cpp
// Construct in-place
protocol<Drawable> p1(std::in_place_type<Circle>);
// 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<Drawable> 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<I>` 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<const Drawable> 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<Drawable> p(std::in_place_type<Circle>);

// 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<Drawable> v1(circle);
protocol_view<Drawable> v2 = v1;
v2.draw();
assert(circle.draw_count() == 1);
```c++
template <typename R, typename... Args>
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 <typename R, typename... Args>
struct MoveOnlyMutatingFunction {
// Deleted copy constructor and copy assignment.
MoveOnlyFunction(const MoveOnlyFunction&) = delete;
MoveOnlyFunction& operator=(const MoveOnlyFunction&) = delete;

`protocol<I>` 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<I>` provides non-owning reference semantics. It is constructible
from any structurally conforming type, including `protocol<I>` itself. Method
calls are forwarded through a synthesised vtable.

Const-correctness is strictly maintained. A `const`-qualified `protocol` object,
or a `protocol_view<const I>`, 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<R(Args...) const>` | `protocol<Function<R, Args...>>` |
| `std::move_only_function<R(Args...) const>` | `protocol<MoveOnlyFunction<R, Args...>>` |
| `std::function_ref<R(Args...) const>` | `protocol_view<Function<R, Args...>>` |
| `std::copyable_function<R(Args...)>` | `protocol<MutatingFunction<R, Args...>>` |
| `std::move_only_function<R(Args...)>` | `protocol<MoveOnlyMutatingFunction<R, Args...>>` |
| `std::function_ref<R(Args...)>` | `protocol_view<MutatingFunction<R, Args...>>` |
| ??? | `protocol<OverloadedFunction>` |
| ??? | `protocol_view<OverloadedFunction>` |

**`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 `<protocol>`."

## 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
<https://github.com/jbcoe/cc-protocol>. The implementation demonstrates the
feasibility of vtable generation, allocator awareness, and the value semantics
properties required by this proposal.

## Acknowledgements

Expand All @@ -246,5 +196,11 @@ this proposal.

[P2996] _Reflection for C++26_. <https://isocpp.org/files/papers/P2996R13.html>

[Metaclasses for generative C++]
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p0707r5.pdf>

[P3086R4 Proxy]
<https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3086r5.html>

[py_cppmodel] _Python wrappers for clang's parsing of C++ to simplify AST
analysis_. <https://github.com/jbcoe/py_cppmodel>
Loading