Skip to content

Commit f845f0b

Browse files
mpuszclaude
andauthored
feat: C++26 contracts support (#817)
* feat: C++26 contracts support added Adds a native C++26 contracts backend (P2900) to the contract-checking machinery, next to the existing gsl-lite/ms-gsl ones: - `MP_UNITS_API_CONTRACTS=STD` CMake option / `contracts=std` Conan option (auto-detected via `__cpp_contracts >= 202502L`); experimental, requires GCC 16 with `-fcontracts` - each macro belongs to exactly one vocabulary now: - `MP_UNITS_EXPECTS*` stay GSL-only (no behavior change for GSL users) - `MP_UNITS_PRE`/`MP_UNITS_POST` map to C++26 `pre`/`post` specifiers on function declarations (piloted on `basic_fixed_string` element access) - `MP_UNITS_PRE_BODY*` are the native-only in-body workaround for functions whose `pre()` predicates GCC cannot constant-evaluate yet - `MP_UNITS_PRECONDITION*` aggregate the two so a check is emitted by whichever backend is active; all unpaired in-body precondition sites migrated to them - `contract_assert` is used only at runtime; during constant evaluation a violation poisons the constant expression instead (GCC 16 fails to constant-evaluate contract predicates in many valid contexts) - `contracts=std` composes with `import_std` and `freestanding` (the old restrictions applied only to third-party GSL headers); a freestanding application has to provide a contract-violation handler or compile with `-fcontract-evaluation-semantic=quick_enforce` - `INTMAX_MAX` replaced with `std::numeric_limits` in contract predicates (the macro is unavailable with `import std;`) - CI job matrix learns C++26 (`max_std` per toolchain) and guarantees `contracts=std` coverage on supporting toolchains; `check_all.sh` gcc16 row now exercises `import_std` together with `std` contracts Resolves #682 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): make C++26 contracts jobs pass on the CI toolchains - hide the runtime `contract_assert` behind the non-constexpr `detail::runtime_contract_check()` helper - some GCC 16 snapshots (e.g. 20260315 used by CI) fail to constant-evaluate contract assertions even in the not-taken branch of `if consteval`, and a non-constexpr function body is invisible to the constant evaluator - disable the freestanding `contracts=std` CI coverage until the upstream libstdc++ regression is fixed (current GCC 16 snapshots reject `-ffreestanding` with `-std=c++26` for any contracts setting) - `check_all.sh` skips configurations for which the environment provides no Conan profile yet (the devcontainer image has no gcc16 profile) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: remove the todo/ design note from the repository The todo/ directory is local-only and must never be committed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent dfb2818 commit f845f0b

26 files changed

Lines changed: 361 additions & 100 deletions

‎.devcontainer/check_all.sh‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,15 @@ conan "$1" . -pr gcc13 -c user.mp-units.build:all=True -o '&:cxx_modules=False
7373
conan "$1" . -pr gcc14 -c user.mp-units.build:all=True -o '&:cxx_modules=False' -o '&:import_std=False' -o '&:std_format=False' -o '&:contracts=ms-gsl' -s compiler.cppstd=23 -b "$build_policy"
7474
conan "$1" . -pr gcc15 -c user.mp-units.build:all=True -o '&:cxx_modules=False' -o '&:import_std=True' -o '&:std_format=True' -o '&:contracts=none' -s compiler.cppstd=26 -b "$build_policy"
7575
set +x
76+
# skip configurations for which this environment provides no Conan profile yet
77+
# (e.g. the devcontainer image lags behind the compiler matrix)
78+
if conan profile show -pr gcc16 > /dev/null 2>&1; then
79+
set -x
80+
conan "$1" . -pr gcc16 -c user.mp-units.build:all=True -o '&:cxx_modules=False' -o '&:import_std=True' -o '&:std_format=True' -o '&:contracts=std' -s compiler.cppstd=26 -b "$build_policy"
81+
set +x
82+
else
83+
echo "⚠️ Skipping gcc16: Conan profile not found in this environment"
84+
fi
7685

7786
echo "⚙️ Testing Clang configurations..."
7887
set -x
@@ -93,6 +102,13 @@ if [[ $run_debug ]]; then
93102
conan "$1" . -pr gcc14 -c user.mp-units.build:all=True -o '&:cxx_modules=False' -o '&:import_std=False' -o '&:std_format=False' -o '&:contracts=ms-gsl' -s compiler.cppstd=23 -b "$build_policy" -s build_type=Debug
94103
conan "$1" . -pr gcc15 -c user.mp-units.build:all=True -o '&:cxx_modules=False' -o '&:import_std=True' -o '&:std_format=True' -o '&:contracts=none' -s compiler.cppstd=26 -b "$build_policy" -s build_type=Debug
95104
set +x
105+
if conan profile show -pr gcc16 > /dev/null 2>&1; then
106+
set -x
107+
conan "$1" . -pr gcc16 -c user.mp-units.build:all=True -o '&:cxx_modules=False' -o '&:import_std=True' -o '&:std_format=True' -o '&:contracts=std' -s compiler.cppstd=26 -b "$build_policy" -s build_type=Debug
108+
set +x
109+
else
110+
echo "⚠️ Skipping gcc16: Conan profile not found in this environment"
111+
fi
96112

97113
echo "⚙️ Testing Clang debug configurations..."
98114
set -x

‎.github/generate-job-matrix.py‎

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def _load_feature_compat() -> dict:
3636

3737

3838
def _make_feature_support(
39-
conan_compiler: str, version: int, *, freestanding: bool = False
39+
conan_compiler: str, version: int, *, freestanding: bool = False, max_std: int = 23
4040
) -> ToolchainFeatureSupport:
4141
"""Derive ToolchainFeatureSupport from conanfile.py's _feature_compatibility."""
4242

@@ -49,7 +49,9 @@ def supports(feature: str) -> bool:
4949
std_format=supports("std_format"),
5050
import_std=supports("import_std"),
5151
explicit_this=supports("explicit_this"),
52+
std_contracts=supports("std_contracts"),
5253
freestanding=freestanding,
54+
max_std=max_std,
5355
)
5456

5557

@@ -63,7 +65,13 @@ def make_gcc_config(version: int) -> Toolchain:
6365
cc=f"gcc-{version}",
6466
cxx=f"g++-{version}",
6567
),
66-
feature_support=_make_feature_support("gcc", version, freestanding=True),
68+
feature_support=_make_feature_support(
69+
"gcc",
70+
version,
71+
freestanding=True,
72+
# -std=c++26 is available since gcc-14
73+
max_std=26 if version >= 14 else 23,
74+
),
6775
)
6876

6977

@@ -77,7 +85,13 @@ def make_clang_config(
7785
version=version,
7886
),
7987
lib="libc++",
80-
feature_support=_make_feature_support("clang", version, freestanding=True),
88+
feature_support=_make_feature_support(
89+
"clang",
90+
version,
91+
freestanding=True,
92+
# -std=c++26 is available since clang-17
93+
max_std=26 if version >= 17 else 23,
94+
),
8195
)
8296
match architecture:
8397
case "x86-64":
@@ -96,7 +110,9 @@ def make_clang_config(
96110
std_format=cfg.feature_support.std_format,
97111
import_std=False,
98112
explicit_this=cfg.feature_support.explicit_this,
113+
std_contracts=cfg.feature_support.std_contracts,
99114
freestanding=cfg.feature_support.freestanding,
115+
max_std=cfg.feature_support.max_std,
100116
)
101117
case _:
102118
raise KeyError(f"Unsupported architecture {architecture!r} for Clang")
@@ -160,7 +176,7 @@ def make_msvc_config(release: str) -> Toolchain:
160176

161177
full_matrix = dict(
162178
toolchain=list(toolchains.values()),
163-
std=[20, 23],
179+
std=[20, 23, 26],
164180
build_type=["Release", "Debug"],
165181
**ConanOptions.full_matrix(),
166182
)
@@ -174,9 +190,22 @@ def _guarantee_api_coverage(
174190
freestanding: bool,
175191
contracts: str | None = None,
176192
) -> None:
177-
"""Guarantee ≥1 import_std and ≥1 no_crtp configuration per supporting toolchain."""
193+
"""Guarantee ≥1 import_std, ≥1 no_crtp, and ≥2 C++26-contracts configurations
194+
per supporting toolchain."""
178195
no_crtp_extra = {} if contracts is None else {"contracts": contracts}
179196
for tc in toolchains_iter:
197+
# C++26 contracts require C++26; two samples so that later
198+
# `min_samples_per_value=2` passes do not have to hit this narrow
199+
# configuration by chance (the freestanding preset adds its own coverage).
200+
if tc.feature_support.std_contracts and not freestanding and contracts is None:
201+
collector.sample_combinations(
202+
rgen=rgen,
203+
min_samples=2,
204+
toolchain=tc,
205+
contracts="std",
206+
std=26,
207+
freestanding=freestanding,
208+
)
180209
# import_std is incompatible with freestanding: the pre-built std.pcm
181210
# is compiled without -ffreestanding and cannot be reused.
182211
if tc.feature_support.import_std and not freestanding:
@@ -293,6 +322,13 @@ def main():
293322
rgen=rgen,
294323
min_samples_per_value=1,
295324
toolchain=latest_clang,
325+
# exclude contract backends the toolchain does not support so the
326+
# sampler is not asked for unsatisfiable combinations
327+
contracts=[
328+
c
329+
for c in ConanOptions.full_matrix()["contracts"]
330+
if c != "std" or latest_clang.feature_support.std_contracts
331+
],
296332
freestanding=False,
297333
)
298334
case "all-freestanding":
@@ -327,6 +363,17 @@ def main():
327363
std_format=False,
328364
**base,
329365
)
366+
# TODO C++26 contracts in freestanding mode are supported by the build system
367+
# but not CI-covered yet: current GCC 16 snapshots reject `-ffreestanding`
368+
# with `-std=c++26` for ANY contracts setting (libstdc++ regression:
369+
# "'range_format' does not name a type" in <optional> when <ranges> is
370+
# included). Add coverage here when upstream is fixed:
371+
# collector.all_combinations(
372+
# filter=lambda me: me.toolchain.feature_support.std_contracts,
373+
# toolchain=freestanding_toolchains,
374+
# std_format=True,
375+
# **{**base, "contracts": "std", "std": 26},
376+
# )
330377
collector.sample_combinations(
331378
rgen=rgen,
332379
min_samples_per_value=1,

‎.github/job_matrix.py‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class ToolchainFeatureSupport:
2424
import_std: bool = False
2525
freestanding: bool = False
2626
explicit_this: bool = False
27+
std_contracts: bool = False
28+
max_std: int = 23
2729

2830

2931
@dataclass(frozen=True, order=True, kw_only=True)
@@ -32,7 +34,7 @@ class ConanOptions:
3234
import_std: bool
3335
std_format: bool
3436
no_crtp: bool
35-
contracts: typing.Literal["none", "gsl-lite", "ms-gsl"]
37+
contracts: typing.Literal["none", "std", "gsl-lite", "ms-gsl"]
3638
freestanding: bool
3739

3840
def is_supported_on(self, feat: ToolchainFeatureSupport) -> bool:
@@ -42,6 +44,10 @@ def is_supported_on(self, feat: ToolchainFeatureSupport) -> bool:
4244
explicit_this = feat.pop("explicit_this")
4345
if self.no_crtp and not explicit_this:
4446
return False
47+
std_contracts = feat.pop("std_contracts")
48+
if self.contracts == "std" and not std_contracts:
49+
return False
50+
feat.pop("max_std") # checked in Configuration.is_supported (needs std)
4551
# now, the rest
4652
for k, v in feat.items():
4753
if getattr(self, k) and not v:
@@ -83,7 +89,7 @@ def for_github(self):
8389
@dataclass(frozen=True, order=True, kw_only=True)
8490
class Configuration(ConanOptions):
8591
toolchain: Toolchain
86-
std: typing.Literal[20, 23]
92+
std: typing.Literal[20, 23, 26]
8793
build_type: typing.Literal["Release", "Debug"]
8894

8995
@property
@@ -92,14 +98,24 @@ def is_supported(self) -> bool:
9298
# check if selected features are supported by the toolchain
9399
if not self.is_supported_on(self.toolchain.feature_support):
94100
return False
101+
# maximum standard supported by the toolchain
102+
if self.std > self.toolchain.feature_support.max_std:
103+
return False
95104
# minimum standard
96105
if self.std < 23 and any([self.import_std, self.no_crtp]):
97106
return False
107+
if self.std < 26 and self.contracts == "std":
108+
return False
109+
# third-party contract libraries require a hosted implementation
110+
if self.freestanding and self.contracts not in ("none", "std"):
111+
return False
98112
# additional checks for import_std
99113
if self.import_std:
100114
if not self.std_format:
101115
return False
102-
if self.contracts != "none":
116+
# third-party contract libraries textually include std headers, which
117+
# conflict with `import std;`; C++26 contracts are a language feature
118+
if self.contracts not in ("none", "std"):
103119
return False
104120
# MSVC Debug + std::format trips error C7595 ("call to immediate function is not a
105121
# constant expression") on heavily templated mp-units quantity arguments — the

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ This page documents the version history and changes for the **mp-units** library
99
### 2.6.0 <small>TBD</small> { id="2.6.0" }
1010

1111
- (!) feat: `pi` and `π` is now a unit constant
12+
- feat: C++26 contracts support added (`contracts=std` Conan option / `MP_UNITS_API_CONTRACTS=STD`
13+
CMake option); experimental, requires GCC 16 with `-fcontracts` (#682)
14+
- feat: `MP_UNITS_PRECONDITION`, `MP_UNITS_PRECONDITION_DEBUG`, `MP_UNITS_PRE`, `MP_UNITS_POST`,
15+
`MP_UNITS_PRE_BODY`, and `MP_UNITS_PRE_BODY_DEBUG` compatibility macros added;
16+
`MP_UNITS_EXPECTS*` are now GSL-backends-only (unchanged there, no-op under C++26
17+
contracts) (#682)
18+
- feat: `contracts=std` may be combined with `import_std` (the GSL-era restriction applied
19+
only to third-party library headers) (#682)
20+
- feat: `contracts=std` may be combined with `freestanding`; the application has to provide
21+
a contract-violation handler or compile with an evaluation semantic that does not
22+
call one (e.g. `quick_enforce`) (#682)
23+
- fix: `INTMAX_MAX` macro replaced with `std::numeric_limits<std::intmax_t>::max()` in
24+
contract predicates (the macro is unavailable with `import std;`)
1225
- (!) feat: natural units reworked from scratch
1326
- (!) feat: IAU system definition improved
1427
- feat: comparisons against literal `0`

‎conanfile.py‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class MPUnitsConan(ConanFile):
5959
"import_std": [True, False],
6060
"std_format": [True, False],
6161
"no_crtp": [True, False],
62-
"contracts": ["none", "gsl-lite", "ms-gsl"],
62+
"contracts": ["none", "std", "gsl-lite", "ms-gsl"],
6363
"freestanding": [True, False],
6464
}
6565
default_options = {
@@ -131,6 +131,15 @@ def _feature_compatibility(self):
131131
"msvc": "195",
132132
},
133133
},
134+
"std_contracts": {
135+
"min_cppstd": "26",
136+
"compiler": {
137+
"gcc": "16",
138+
"clang": "",
139+
"apple-clang": "",
140+
"msvc": "",
141+
},
142+
},
134143
}
135144

136145
@property
@@ -250,15 +259,22 @@ def validate(self):
250259
for key, value in self._option_feature_map.items():
251260
if self.options.get_safe(key) == True:
252261
self._check_feature_supported(key, value)
253-
if self.options.freestanding and self.options.contracts != "none":
262+
if self.options.contracts == "std":
263+
self._check_feature_supported("contracts", "std_contracts")
264+
# third-party contract libraries require a hosted implementation; C++26 contracts
265+
# are a language feature (the application has to provide a contract-violation
266+
# handler or compile with an evaluation semantic that does not need one)
267+
if self.options.freestanding and self.options.contracts not in ["none", "std"]:
254268
raise ConanInvalidConfiguration(
255-
"'contracts' should be set to 'none' for a freestanding build"
269+
"'contracts' should be set to 'none' or 'std' for a freestanding build"
256270
)
257271
# TODO mixing of `import std;` and regular header files includes does not work for now
258272
if self.options.import_std:
259-
if self.options.contracts != "none":
273+
# C++26 contracts are a language feature and do not bring any third-party
274+
# library headers that would conflict with `import std;`
275+
if self.options.contracts not in ["none", "std"]:
260276
raise ConanInvalidConfiguration(
261-
"'contracts' should be set to 'none' to use `import std;`"
277+
"'contracts' should be set to 'none' or 'std' to use `import std;`"
262278
)
263279
if not self.options.get_safe("std_format", default=True):
264280
raise ConanInvalidConfiguration(
@@ -366,6 +382,17 @@ def package_info(self):
366382
self.cpp_info.components["core"].defines.append(
367383
"MP_UNITS_API_CONTRACTS=0"
368384
)
385+
elif self.options.contracts == "std":
386+
self.cpp_info.components["core"].defines.append(
387+
"MP_UNITS_API_CONTRACTS=1"
388+
)
389+
if self.settings.compiler == "gcc":
390+
self.cpp_info.components["core"].cxxflags.append("-fcontracts")
391+
if not self.options.freestanding:
392+
# the default contract-violation handler lives in the experimental
393+
# library; a freestanding application has to provide its own handler
394+
# (or compile with `-fcontract-evaluation-semantic=quick_enforce`)
395+
self.cpp_info.components["core"].system_libs.append("stdc++exp")
369396
elif self.options.contracts == "gsl-lite":
370397
self.cpp_info.components["core"].requires.append("gsl-lite::gsl-lite")
371398
self.cpp_info.components["core"].defines.append(

‎docs/getting_started/cpp_compiler_support.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ a specific C++ feature:
2020
| **C++ modules** | 20 | None | 17+ | None | None |
2121
| **`import std;`** | 23 | 15+ | 18+ | None | 19.5+ |
2222
| **Explicit `this` parameter** | 23 | 14+ | 18+ | 17+ | 19.5+ |
23+
| **Contracts** | 26 | 16+ | None | None | None |
2324

2425
??? note "Clang-19 unfixable bug"
2526

@@ -98,4 +99,23 @@ a specific C++ feature:
9899
- Conan: [no_crtp](installation_and_usage.md#no_crtp)
99100
- CMake: [MP_UNITS_API_NO_CRTP](installation_and_usage.md#MP_UNITS_API_NO_CRTP)
100101

102+
103+
## Contracts
104+
105+
- If enabled, the library's preconditions and assertions are checked with C++26 contract
106+
assertions instead of a GSL library.
107+
- A violation during constant evaluation is reported as a compilation error, and a runtime
108+
violation is reported through the compiler's contract-violation handler.
109+
- Tested with `__cpp_contracts` [feature test macro](https://en.cppreference.com/w/cpp/feature_test).
110+
- The build system adds the required compiler-specific flags (e.g. `-fcontracts` and linking
111+
`stdc++exp` for gcc).
112+
- May be combined with `import_std` (contracts are a language feature, so no third-party
113+
headers conflict with importing the standard library) and with `freestanding` (the default
114+
contract-violation handler needs a hosted implementation, so a freestanding application
115+
has to provide its own handler or compile with an evaluation semantic that does not call
116+
one, e.g. gcc's `-fcontract-evaluation-semantic=quick_enforce`).
117+
- Related build options:
118+
- Conan: [contracts](installation_and_usage.md#contracts)
119+
- CMake: [MP_UNITS_API_CONTRACTS](installation_and_usage.md#MP_UNITS_API_CONTRACTS)
120+
101121
*[CRTP]: Curiously Recurring Template Parameter

‎docs/getting_started/installation_and_usage.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,16 @@ If you obtain them differently you may need to adjust some CMake files.
142142

143143
#### `contracts`
144144

145-
: [:octicons-tag-24: 2.2.0][release-2-2-0] · :octicons-milestone-24: `none`/`gsl-lite`/`ms-gsl`
145+
: [:octicons-tag-24: 2.2.0][release-2-2-0] · :octicons-milestone-24: `none`/`std`/`gsl-lite`/`ms-gsl`
146146
(Default: see below)
147147

148148
Enables precondition checks and additional assertions.
149149

150150
If `import_std` defaults to `True`, `contracts` defaults to `none`; otherwise `gsl-lite`.
151151

152+
`std` uses C++26 contract assertions and requires an experimental compiler
153+
(currently only GCC 16 with `-fcontracts`).
154+
152155
#### `freestanding`
153156

154157
: [:octicons-tag-24: 2.2.0][release-2-2-0] · :octicons-milestone-24: `True`/`False`
@@ -209,10 +212,13 @@ If you obtain them differently you may need to adjust some CMake files.
209212
[`MP_UNITS_API_CONTRACTS`](#MP_UNITS_API_CONTRACTS){ #MP_UNITS_API_CONTRACTS }
210213

211214
: [:octicons-tag-24: 2.2.0][release-2-2-0] · :octicons-milestone-24:
212-
`NONE`/`GSL-LITE`/`MS-GSL` (Default: `GSL-LITE`)
215+
`NONE`/`STD`/`GSL-LITE`/`MS-GSL` (Default: `GSL-LITE`)
213216

214217
Enables checking of preconditions and additional asserts in the code.
215218

219+
`STD` uses C++26 contract assertions and requires an experimental compiler
220+
(currently only GCC 16 with `-fcontracts`).
221+
216222
[`MP_UNITS_API_FREESTANDING`](#MP_UNITS_API_FREESTANDING){ #MP_UNITS_API_FREESTANDING }
217223

218224
: [:octicons-tag-24: 2.2.0][release-2-2-0] · :octicons-milestone-24:

0 commit comments

Comments
 (0)