C++ Contracts ABI: Design Requirements¶
Purpose¶
This document establishes the requirements for the Itanium C++ ABI specification for contracts, a feature arriving in C++26. These requirements will guide the evaluation and selection of design approaches.
The goal is straightforward: define a stable, extensible interface between compilers and runtime libraries for handling contract violations. When a contract fails, the compiler must arrange for the user’s violation handler to be called with a std::contract_violation object containing information about what went wrong.
Background¶
C++26 introduces contracts—preconditions, postconditions, and assertions that express program invariants directly in code. When a contract fails at runtime, the program needs to construct a std::contract_violation object and invoke the user’s registered handler.
The challenge lies in the details. Compilers (GCC, Clang) and standard libraries (libstdc++, libc++) are developed independently. The ABI must work across all combinations of these tools, both today and as they evolve through future C++ standards. It must accommodate future extensions we can’t yet anticipate. And it must do all this efficiently, without bloating binaries or slowing down optimized code.
Two main approaches are under consideration:
- Descriptor Table Approach
The compiler emits metadata tables describing contract data layout. At runtime, the library parses these descriptors to construct the violation object.
- Direct Construction Approach
The compiler generates code that directly builds the violation object on the stack, then calls the handler. No runtime parsing, no metadata tables.
This document captures what any viable approach must achieve.
Critical Requirements¶
ABI Stability¶
The interface between compiler and runtime must remain stable as tools evolve. Specifically:
- Entrypoint stability
Function signatures that compilers call cannot change in incompatible ways. If an entrypoint exists, its calling convention must remain fixed.
- Data structure stability
Any data structures passed across the ABI boundary must support evolution. Existing fields must remain at stable offsets. New fields can only be added in ways that don’t break existing code.
- Standard type evolution
The
std::contract_violationtype may gain new fields in future C++ standards (C++29, C++32…). The ABI must accommodate this growth without requiring recompilation of existing code or breaking old binaries.
The key constraint: once released, the ABI cannot break existing object files.
Validation: See test_abi_forward_compatibility and test_abi_backward_compatibility for validation scenarios.
Cross-Compiler Interoperability¶
Object files compiled by any conforming compiler must work with any conforming standard library, provided both support the same C++ standard version.
- For example:
Clang-compiled code using C++26 contracts must work with any libstdc++ that supports C++26 contracts
GCC-compiled code using C++26 contracts must work with any libc++ that supports C++26 contracts
Mixed compilation (different parts compiled by different compilers) must work
This requirement rules out solutions that tightly couple a specific compiler version to a specific library version.
Validation: See test_cross_compiler_interop for validation scenarios.
Forward Compatibility¶
- New object files with old runtimes
When a newer compiler emits object files with additional contract data fields, older runtime libraries must handle them gracefully. The old runtime won’t understand the new fields—and that’s fine. It should use the fields it recognizes and silently ignore what it doesn’t understand.
- Old object files with new runtimes
New runtime libraries must correctly process contract violations from older object files, even when expected fields are missing. Missing fields should map to appropriate default values.
This allows gradual toolchain updates without requiring everything to move in lockstep.
Backward Compatibility¶
- New compiler with old runtime
A C++26 compiler should be able to emit contract violations that work with standard library implementations that only support basic C++26 contracts, even if the compiler itself supports more advanced features from later standards.
- Version skew tolerance
Projects often use different compiler versions across components, or link against system libraries from earlier releases. The ABI must tolerate reasonable version differences.
Constructor Isolation¶
When a contract violation occurs, the program is in a potentially invalid state. Running arbitrary user code between detecting the violation and invoking the handler creates safety risks.
- Must not invoke
User-provided constructors for types in user code must not be called during contract violation processing.
- May need to invoke
Constructors for standard library types (
std::string_view,std::source_location, potentiallystd::exception_ptr) may need to be invoked. Future C++ standards might add fields tostd::contract_violationthat require non-trivial construction.
The distinction matters because compilers don’t see standard library headers at contract sites—they compile contracts without any #include directives. This makes it difficult for compilers to know how to construct standard library types, especially given implementation details like inline namespaces (std::__libcpp::exception_ptr vs std::exception_ptr).
An approach that requires the compiler to construct objects with standard library types faces challenges here. An approach that delegates construction to the runtime library avoids this problem.
Validation: See test_constructor_isolation for validation scenarios.
Efficient Field Omission¶
Users must be able to omit certain contract data at compile time to save space:
Source file locations (when stripped builds are required)
Source text strings (can be large, especially for complex predicates)
Vendor-specific diagnostic fields
- True zero overhead
When a field is omitted, it should cost nothing—no storage in object files, no initialization code generated, no runtime overhead.
- No version explosion
Supporting omission should not require creating exponentially many versions. If there are N potentially omittable fields, the design should not require 2^N variants to avoid wasted space.
This requirement is driven by large-scale C++ projects that already push linker limits. Adding contracts should not force these projects to bloat their binaries.
Validation: See test_efficient_field_omission for validation scenarios.
Minimal Code Generation Impact¶
Contracts should not make programs slower or harder to optimize.
- Cold path isolation
Contract failure code is a cold path—it rarely executes. This code should not inhibit optimization of the hot path. Specifically:
It shouldn’t cause inlining budget exhaustion
It shouldn’t pollute instruction caches
It shouldn’t prevent tail calls or other optimizations
- Compact per-contract code
The code generated at each contract site should be minimal. For projects with thousands of contracts, per-contract overhead multiplies quickly.
- Small object file growth
The total size added to object files should scale reasonably. This includes both code size and data size (.rodata sections for strings, metadata, etc.).
The constraint: large projects with giant object files at linker limits should be able to adopt contracts without hitting new limits.
Validation: See test_minimal_code_generation for validation scenarios.
Important Requirements¶
Vendor Extensibility¶
Compiler vendors need to add proprietary extensions:
Additional diagnostic messages
Stack traces or call context
Performance counters
Integration with sanitizers or debugging tools
- Independent extension
Vendors should be able to add extensions without requiring tight coordination with other vendors or waiting for ABI committee approval for every addition.
- Cross-vendor visibility (optional)
If two vendors independently implement the same extension (e.g., stack traces), it would be useful if they could interoperate. But this is a nice-to-have, not a must-have.
- Future standard features
When the C++ standard adds new contract features, vendors need to be able to implement them without breaking existing deployed code.
Validation: See test_vendor_extensions_independent for validation scenarios.
Small Specification Surface¶
A smaller specification is easier to agree upon, implement correctly, and maintain over time. Minimize the number of:
Required function signatures
Data structure layouts
Enumeration values
Coordination points between vendors
Each piece of standardized interface is a commitment that must remain stable forever.
Validation: See test_small_specification for validation scenarios.
Minimal Governance Overhead¶
Avoid requiring centralized registries or frequent coordination for routine extensions:
Vendor IDs
Field type enumerations
Version number allocation
Some governance is acceptable when it provides clear benefits, but minimize the need for vendors to coordinate on day-to-day development.
Deployment Flexibility¶
Different deployment scenarios have different constraints:
- Platform deployment
Some platforms update compilers and high-level standard libraries (libc++, libstdc++) more frequently than low-level ABI libraries (libc++abi, libsupc++). It would be valuable to support contracts by updating only the compiler and high-level library, without requiring changes to the low-level ABI library.
However, this requirement needs validation. Who actually updates their standard library without updating their ABI library? Apple? Embedded systems? Linux distributions? The answer affects whether this requirement is critical or merely nice-to-have.
- Long-lived deployments
Enterprise systems may run for years with stable OS libraries. Contracts support should work with existing runtime libraries when possible.
Validation: See test_deployment_flexibility for validation scenarios.
Non-Goals¶
User-Provided Constructors¶
The design explicitly does not support calling user-provided constructors during contract violation processing. This means:
- Not supported
Custom types with user-defined constructors cannot be fields in the violation object that the compiler constructs.
- Rationale
The program is already in an invalid state when a contract fails. Running arbitrary user code (constructors, operators, conversions) between violation detection and handler invocation creates safety and reentrancy hazards.
- Clarification
This restriction applies to user code, not standard library code. The runtime library may need to construct standard library types as part of building the
std::contract_violationobject. The distinction is that the standard library is part of the contracts implementation itself, not arbitrary user code running in an invalid state.
Validation: See test_reject_user_constructors for validation scenarios.
Design Questions¶
These questions affect requirement priorities and should be resolved:
- Optimization authority
Should the specification mandate specific optimization strategies (for size, compile time, runtime), or leave these choices to vendors?
Baking in optimization decisions ensures consistency across implementations but constrains vendor innovation. Leaving optimization to vendors allows flexibility but may lead to behavioral differences.
- Platform deployment constraint
How important is it to support contracts without updating low-level ABI libraries? This affects whether the entrypoint must live in libc++/libstdc++ (higher-level) versus libc++abi/libsupc++ (lower-level).
The answer depends on real-world deployment patterns, which may vary across platforms.
Evaluation Criteria¶
Proposed approaches should be evaluated against these criteria:
- Compatibility
Does it support all required interoperability scenarios?
How does it handle version skew?
Can old and new code coexist?
- Extensibility
How easily can new fields be added?
What coordination is required for vendor extensions?
Does it accommodate unknown future requirements?
- Efficiency
What is the per-contract code size?
How efficient is field omission?
What runtime overhead does it impose?
- Specification complexity
How much must be standardized?
How many coordination points exist?
How easy is it to implement correctly?
- Standard library flexibility
Does it lock down
std::contract_violationlayout?Can standard libraries use different implementations?
How does it handle STL type construction?
Trade-Offs¶
No design will optimize all requirements simultaneously. Some trade-offs are fundamental:
- Simplicity vs. Flexibility
Simpler specifications (e.g., mandating a fixed memory layout) are easier to understand but constrain future evolution. More flexible specifications (e.g., descriptor-based indirection) support richer evolution but add complexity.
- Compiler work vs. Runtime work
Work must happen somewhere. Either the compiler generates more code (construction thunks, data structures), or the runtime does more work (parsing, interpretation). The question is which approach scales better.
- Code size vs. Runtime cost
Smaller per-contract code may require more runtime interpretation. Larger per-contract code may reduce runtime overhead. The optimal balance depends on the ratio of contracts to executions.
- Vendor independence vs. Coordination
Allowing vendors to act independently simplifies their development process but may lead to incompatibilities. Requiring coordination ensures compatibility but slows evolution.
Understanding these trade-offs helps evaluate whether a proposed approach makes the right compromises.
Requirements Validation¶
Concrete test scenarios for validating that an ABI implementation satisfies these requirements are documented in the Test Cases document. These tests are implementation-agnostic and should work regardless of which approach (descriptor table, runtime-constructed, or other) is adopted.