Skip to content

[feat] Add a metadata policy to proxy - #90

Open
mingxwa wants to merge 3 commits into
ngcpp:feature/v5from
mingxwa:user/mingxwa/metadata-policy
Open

mingxwa wants to merge 3 commits into
ngcpp:feature/v5from
mingxwa:user/mingxwa/metadata-policy

Conversation

@mingxwa

@mingxwa mingxwa commented Sep 22, 2026

Copy link
Copy Markdown
Member

proxy takes a second template parameter that decides how an invocation is erased and where the metadata lives. compact_metadata is the default and keeps the current layout. inline_metadata keeps all metadata inside the proxy, trading size for one fewer indirection per invocation. This helps latency-sensitive code that invokes a long-lived proxy.

Changes

  • Added compact_metadata, inline_metadata and the ProMetadataPolicy named requirement. A policy is checked by static_assert where the proxy naming it is instantiated.
  • Threaded the policy through proxy, proxy_indirect_accessor, proxiable, inplace_proxiable_target, proxiable_target, proxy_view, weak_proxy and the creation functions. Overloads that deduce the target type keep their signatures and use the default policy, except make_proxy_view, which takes the policy right after the facade.
  • Replaced facade_aware_overload_t with proxy_dependent_signature, which instantiates its template with the facade and the policy. This lets skills::as_view, skills::as_weak and weak_proxy::lock keep the policy of the source proxy.
  • Renamed facade_meta_traits.h to metadata_policy.h, facade_traits to proxy_traits, inplace_meta_storage to inline_meta_storage, and ProBasicMeta and ProMeta to ProBasicMetadata and ProMetadata to fit the new vocabulary.
  • Required ProBasicMetadata types, including reflector types, to be class types, so an invoker that cannot be inherited is rejected by the policy check.
  • Simplified nullable to basic metadata that converts to bool, and removed the reset() and has_value() members that restated existing operations.
  • Fixed copying a null invoker or metadata pointer under pointer authentication. This also fixes swapping an empty proxy with a non-empty one under trivial relocation on arm64e.
  • Added unit tests for both policies, their invoker and their storages.

Pure moves with no change in content, so that history follows each
file across the rename. The next commit updates every reference to
them, so this commit neither builds nor passes `mkdocs build --strict`
on its own.

- `facade_meta_traits.h` becomes `metadata_policy.h`.
- `ProBasicMeta.md` becomes `ProBasicMetadata.md`.
- `ProMeta.md` becomes `ProMetadata.md`.
- `facade_aware_overload_t.md` becomes `proxy_dependent_signature.md`.
Give `proxy` a second template parameter `MetadataPolicy` that decides
how an invocation is erased and how the metadata deduced from the
contained type is kept. `compact_metadata`, the default, keeps small
metadata in the proxy and otherwise points at a static metadata object,
while `inline_metadata` always keeps the metadata in the proxy, trading
size for one fewer indirection per invocation. The latter is aimed at
latency-sensitive code, where the static metadata object of a long-lived
proxy costs a cache line the caller would not otherwise touch.

The metadata type of a `proxy<F, MP>` depends on both `F` and `MP`, so
a proxy converts only to a proxy with the same metadata policy. The
policy is chosen where the proxy type is named, and every conversion to
a super preserves it.

- `proxy_dependent_signature` instantiates its template argument with a
  facade and a metadata policy, so a convention can return a proxy that
  keeps the metadata policy of the proxy it was obtained from.
  `skills::as_view`, `skills::as_weak` and `weak_proxy::lock` do exactly
  that.
- The policy is threaded through `proxy`, `proxy_indirect_accessor`,
  `proxiable`, `inplace_proxiable_target`, `proxiable_target`,
  `proxy_view`, `weak_proxy` and every function template that creates a
  proxy. The overloads that deduce the target type keep their existing
  signatures and always use the default policy, so naming a target type
  can never be mistaken for naming a policy.
- Add the *ProMetadataPolicy* named requirement. A policy is not
  constrained by a concept, and one whose invoker or storage is not
  well-formed is diagnosed by `static_assert` where the proxy naming it
  is instantiated.

Renames that prepare the vocabulary:
- `compact_facade_meta_traits`/`flat_facade_meta_traits` become
  `compact_metadata`/`inline_metadata`.
- `inplace_meta_storage` becomes `inline_meta_storage`, matching the
  policy that selects it.
- `facade_aware_overload_t` becomes `proxy_dependent_signature`, and the
  conventions and signature templates that go with it drop the `faw`
  and `overload` spellings.
- `facade_traits` becomes `proxy_traits`, because it now depends on the
  metadata policy as well as the facade.
- *ProBasicMeta* and *ProMeta* become *ProBasicMetadata* and
  *ProMetadata*.

nullable required a meta to name reset() and has_value(), and both
restate an operation the meta already has. has_value() is the contextual
conversion to bool, and reset() is assignment from a value-initialized
meta, which basic_meta already requires to be well formed and nothrow.
The concept becomes basic_meta plus bool-testability, and the named
members are gone from sentinel_meta, invoker_base, static_meta_storage
and proxy_meta_base_impl. code_ptr and meta_ptr follow, trading
operator==(std::nullptr_t) and operator=(std::nullptr_t) for an explicit
operator bool, so the signed wrapper and the plain pointer alias are
spelled the same way.

Stating the null state as a value-initialized meta moves it to the type
that knows it. composite_meta holds the metas as bases, so it owns which
one carries the null state: it value-initializes that first base and
leaves the rest uninitialized, tests it, and copies the rest only when
it holds a value. Its copy constructor delegates to the default
constructor, because GCC's -Wextra flags a copy constructor that does
not explicitly initialize a base with a user-provided constructor.
proxy_meta_base_impl keeps only the conversion to a contained meta, and
no longer distinguishes the first meta from the rest.

Under pointer authentication a null value is stored unsigned, so copying
one must not authenticate it. code_ptr and meta_ptr now skip the resign
for a null value, in an initialize member that writes the destination
from the source as proxy::initialize does. This also repairs swapping an
empty proxy with a non-empty one, which exchanges the metadata words
directly under trivial relocation and trapped on the null one.
SmallMeta and LargeMeta defaulted their default constructor, which left
them trivially default constructible. MSVC then reports C4268 for the
const static object of static_meta_storage, claiming the compiler
generated default constructor fills it with zeros, and /WX turns that
into an error. The claim is false: the in_place_type constructor runs
and the object holds the expected values.

Every meta the library defines already states its null state in a
user-provided constructor, as composite_meta and proxy_meta_base_impl
do, and the metadata a proxy keeps is always proxy_meta, a member-less
wrapper that MSVC never diagnoses. Spell the test metas the same way
rather than suppress the warning where the storage is declared.
@tian-lt

tian-lt commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

My GPT-6 Astra has some review comments:

Suggestions for PR #90

These suggestions are for PR #90. The overall design looks good. The following changes would make the API easier to use and its documentation clearer.

1. Make invalid-policy handling consistent

The proxiable documentation says that a metadata policy is checked when proxy is instantiated. However, evaluating proxiable also instantiates proxy_traits, which contains the policy static_assert. This means that evaluating the concept can already cause a compile error.

This matters for generic code. A caller may use proxiable to choose between two implementations and expect an unsupported combination to produce false.

Suggestion: Let proxiable return false when a policy is missing required types or operations. Keep the helpful static_assert message for code that actually instantiates proxy with that policy. If a compile error is the intended behavior even when evaluating the concept, please update the documentation to say so.

There is also a small improvement to the policy validator: move the expression that checks *storage into a requires-expression. Currently, a storage type without operator* causes a compile error inside the validator instead of making it return false.

2. Be precise about empty values and noexcept

The nullable requirement says that a "default-constructed" object is empty. In C++, these two forms are different:

I a;    // Default-initialization
I b{};  // Value-initialization

For the current built-in invoker types, the first form can leave the internal pointer uninitialized. The second form initializes it to null.

Suggestion: Describe the empty-state requirement using I{} and S{} explicitly. This makes the requirement clear without changing the implementation.

The exception requirements would also benefit from a small clarification. Saying that an operation "shall not throw" is not the same as declaring it noexcept. A noexcept declaration lets generic code discover the guarantee through type traits and requires-expressions.

For an invoker that represents a noexcept overload, I suggest making its call operator noexcept as well.

3. Make policy selection easier in factory functions

The current API selects a policy in different positions:

make_proxy<F, T, inline_metadata>(value);
make_proxy_view<F, inline_metadata>(value);

The difference is understandable because the functions have different overloads. The main limitation is that make_proxy requires callers to name T when selecting a non-default policy, even when T could otherwise be deduced from value.

Suggestion: Keep the existing meaning of <F, T> so that existing calls continue to work. If selecting a policy while deducing T is an important use case, consider an additional overload that accepts a policy tag as a function argument.

If that extra overload is not needed, the current design is reasonable. Just keep the limitation clearly documented.

4. Describe performance benefits as tradeoffs, not guarantees

The documentation says that inline metadata removes an indirection on every invocation. However, compact_metadata already stores small metadata inline, so those cases do not gain this benefit.

Larger inline metadata also makes each proxy larger. It may occupy more cache lines and reduce the number of proxies that fit in the cache. Therefore, inline storage is not always faster.

Suggested wording:

When compact_metadata stores metadata outside the proxy, inline_metadata removes the indirection needed to access that metadata. This may reduce invocation latency, but it also makes the proxy larger. The performance benefit depends on the metadata size, access patterns, and cache behavior.

For object size, it would also be clearer to mention alignment and padding rather than describe the size as an exact sum:

The proxy contains storage for the pointer-like value and the metadata representation. Its total size also depends on alignment and padding.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants