Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR upgrades the codebase to require C++20 as the minimum standard for extension builds. The change modernizes the codebase by replacing older C++17 constructs and string formatting approaches with C++20 features.
Key changes:
- Updates minimum C++ standard requirement from C++17 to C++20
- Replaces
std::ostringstreamstring formatting with C++20std::format - Modernizes template syntax using C++20
requiresclauses instead of SFINAE - Updates build configuration and CI workflows to use C++20
Reviewed Changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| CMakeLists.txt | Updates minimum C++ standard requirement from 17 to 20 |
| src/treespec/unflatten.cpp | Replaces ostringstream with std::format, adds std::span usage |
| src/treespec/treespec.cpp | Extensive modernization with std::format, std::span, std::ranges, and constinit |
| src/treespec/traversal.cpp | Updates string formatting to use std::format |
| src/treespec/serialization.cpp | Uses std::unordered_map::contains instead of find comparison |
| src/treespec/richcomparison.cpp | Replaces std algorithm calls with std::ranges equivalents |
| src/treespec/hashing.cpp | Uses std::unordered_map::contains for cleaner code |
| src/treespec/flatten.cpp | Comprehensive string formatting updates and template parameter renaming |
| src/treespec/constructors.cpp | String formatting modernization and std::ranges usage |
| src/registry.cpp | Updates string formatting and replaces PYBIND11_CONSTINIT with constinit |
| src/optree.cpp | String formatting updates and constinit usage |
| include/optree/treespec.h | Updates function signatures to use std::span and modernizes template parameters |
| include/optree/pytypes.h | Adds std::formatter specializations and modernizes template syntax with requires clauses |
| include/optree/pymacros.h | Replaces PYBIND11_CONSTINIT with constinit |
| include/optree/exceptions.h | Major refactoring using std::source_location and std::format for error handling |
| README.md | Updates documentation to reflect C++20 requirement |
| CPPLINT.cfg | Adds whitespace/braces filter |
| .github/workflows/tests.yml | Removes C++17 compatibility test |
| .github/workflows/lint.yml | Updates clang-tidy to use C++20 |
| .clang-format | Updates standard from c++17 to c++20 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #235 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 15 15
Lines 1623 1623
Branches 218 218
=========================================
Hits 1623 1623 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
9388aa9 to
b3e5183
Compare
|
6887698 to
c0e6a1d
Compare
ce0ec4b to
5af5e9b
Compare
430d021 to
f35b9b6
Compare
74793af to
947195f
Compare
b834e96 to
4caf09f
Compare
b40c207 to
0b08991
Compare
84639eb to
2d05490
Compare
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Merge the correctness-audit batch from `main` into the C++20 modernization branch. The two sides are orthogonal in intent but heavily co-located in text: `main` fixes semantics (lock ordering, GC reporting, namespace-merge safety, pickle validation), while `cpp20` rewrites the same lines into C++20 idioms (`std::format`, `std::span`, `std::source_location`, `std::ranges`, `requires`, `.contains()`). Every conflict is therefore resolved as *their* logic expressed in *our* idiom, never by picking a side. Explicit conflicts (18 hunks across 9 files): * `exceptions.h`: our include set plus their `<utility>` for `std::declval`. * `pytypes.h`: `[[nodiscard]]` combined with `requires` on `TupleGetItemAs`, `ListGetItemAs` and `DictGetItemAs`; their copy-then-commit `TotalOrderSort` with our `std::format` sort key; `MAX_TYPE_CACHE_SIZE` moved beside their `WeakKeyCache`. * `registry.h`: their `DictInsertionOrderedFlags` snapshot with `.contains()`. * `treespec.h`: our `std::span<py::object>` `MakeNode`; their C-array NOLINT re-tag no longer applies. * `registry.cpp`: their `RegistryStatus` restructure taken wholesale, with our `std::format` messages re-applied at the new post-unlock site. * `flatten.cpp`: their `leaves.size() != num_leaves` condition, our message. * `richcomparison.cpp`, `traversal.cpp`: unioned includes. * `treespec.cpp`: their `sorted_other_keys` snapshot and recursion `depth` argument, our `std::format` and `std::ranges::reverse`. Implicit conflicts (clean merges that split the codebase's idiom): * Converted the five single-shot messages `main` added as `std::ostringstream` to `std::format`, and its four new `find() != end()` to `.contains()` and one `enable_if_t` to `requires`. Branchy message builders stay `ostringstream`, matching the convention on both sides. * `serialization.cpp` used `std::format` without including `<format>`, and its `malformed` lambda still concatenated with `+`. * `RelpathFromProjectRoot()` is now total: a translation unit whose path is shorter than this header's would have thrown `std::out_of_range` out of `PyInit__C`. * Dropped four `NOLINT[...pointer-arithmetic]` comments made dead by `std::span::operator[]`. Keeping the two type-fixing `std::formatter` specializations is load-bearing. Collapsing them into one constrained on `std::is_base_of_v<py::handle, T>` gives the same template argument list as the standard range formatter, and since neither constraint subsumes the other, every pybind11 wrapper becomes ambiguous from C++23 on. That regression was introduced during this merge, caught by review, and reverted; all 11 translation units now compile clean at both C++20 and C++23 on libc++ and libstdc++. Swept the tree for C++20 features neither branch had applied, rather than only checking that each branch's own changes survived: * `std::memory_order_acquire` / `_release` -> `std::memory_order::acquire` / `::release`. C++20 made `std::memory_order` a scoped enum (P0439R0) and keeps the unscoped names only as compatibility constants. These arrived with the new `PyTreeIter` re-entrancy guard from `main`, so neither branch had converted them. * `has_to_string` was a `std::void_t` detection idiom built from a partial specialization over `std::true_type` / `std::false_type`; it is now a concept. Constraining on `const T &` also matches how `try_to_string` actually calls `std::to_string`, where the old form probed an rvalue. `exceptions.h` no longer needs `<type_traits>` or `<utility>`. * `std::cmp_less` (P0586R2) replaces a `static_cast<ssize_t>` around a signed/unsigned comparison in `FromPicklable`. * One residual idiom split: `flatten.cpp`'s dictionary key mismatch still built its message with `ostringstream` while the identical message in `treespec.cpp` had been converted. It now uses `std::format` with `NodeKindToString()`, which already is the single source of truth for those three kind names. Neither of the first two is reported by clang-tidy: there is no check for the scoped `memory_order` enumerators, and `modernize-use-constraints` only rewrites `std::enable_if`, not `void_t` detection. The rest of the surface is clean: no non-ranges algorithms, erase-remove idioms, `compare(0, n, ...)` prefix tests, C arrays, `typedef`, `std::bind` or raw pointer-and-size parameter pairs remain. Give the registry's pair keys transparent hashers and comparators whose call operators are templates, and declare the two containers with them. An `is_transparent` marker only tells the container it may forward a foreign key type; a non-template call operator then converts that key back to the exact `key_type`, which is the very temporary heterogeneous lookup exists to avoid. The marker was inert for unordered containers before P0919R3, so this cost nothing until the standard moved: measured on a standalone model of `PyTreeTypeRegistry::Lookup`, identical source allocates once per `find` at `-std=c++17` and three times at `-std=c++20`. Probing with `std::string_view` takes it to zero, worth -22.9% on flattening a tree of namespaced custom nodes (6063 -> 4676 ns/op; the penalty over a global-namespace flatten drops from +53% to +22%). This is pre-existing on `main`, which already defaults to C++20; it is included here because the branch makes that default unconditional. Also folded in, from reviewing the C++17 -> C++20 bump as a whole: * Set `MACOSX_DEPLOYMENT_TARGET` to 14.0. Apple's libc++ gates `std::format` behind macOS 13.3, but PEP 425 floors the macOS tag's minor version to zero, so a 13.3 binary ships as `macosx_13_0_*`, installs on 13.0 through 13.2 and then fails in `dyld`. 14.0 is the lowest target whose tag does not lie. The previous 15.0 was safe but dropped macOS 14 needlessly. * Retag the iOS wheels to match `IPHONEOS_DEPLOYMENT_TARGET`. cibuildwheel's cross-build environment reports a hardcoded `ios-13.0-*` platform, so the wheels shipped as `ios_13_0_*` while the binary required 16.3; `pip` accepted them on iOS 13.0 through 16.2 and `dyld` then failed to load `_C`. Unlike macOS, iOS tags carry the minor version, so 16.3 is expressible exactly. * Stop passing `-Wno-error=attributes`. It existed so `[[likely]]` and `[[unlikely]]` would not fail a C++17 `-Werror` build and now only masks genuine attribute errors. * Record the breaking standard bump in `CHANGELOG.md` and give `README.md` the concrete compiler floors: GCC 13+, Clang 16+ with libstdc++ 13+, Clang 17+ with libc++ 17+, Apple Clang 16+ (Xcode 16+), or MSVC 19.32+ (VS 2022 17.2+). `std::format` binds on GCC and MSVC; on Clang it is P0634R3. MSVC 19.30 and 19.31 cannot build this at `/std:c++20` at all, since MS STL gated `__cpp_lib_format` behind `_HAS_CXX23` until 17.2, so the floor is not the interval "19.29 and later". Tests: cover the messages this merge rewrote, which had none. Pin the full interpolated text of the namedtuple `_fields` arity mismatch (a swapped placeholder previously matched), the `Malformed pickled PyTreeSpec` template, and the stale-custom-registration rejection from `treespec_from_collection()`. Add `MakeNode`'s empty-`std::span` path across every arity-0 container, including one nested among non-empty siblings. Verified: 94205 passed, 1 skipped, 1 xfailed; clean build at `OPTREE_CXX_WERROR=ON`; all pre-commit hooks pass; every translation unit compiles clean at both C++20 and C++23 under Clang/libc++ and builds and links under GCC 14/libstdc++. clang-tidy reports nothing across all 20 files, run under `quay.io/pypa/manylinux_2_28` because the check set needs a toolchain whose libstdc++ can resolve the C++20 headers -- though note that the project's `NOLINT[...]` bracket syntax is not parsed as a check list by clang-tidy or cpplint, so each marker suppresses every check on its line rather than the ones it names.
Description
Raise the minimum C++ standard from C++17 to C++20 and adopt C++20 features throughout the extension codebase.
C++20 features adopted
std::format: Replace allstd::ostringstreamandoperator+string concatenation for error/exception messages withstd::format. Addstd::formatterspecializations forpy::handleandpy::object.std::source_location: Replace__FILE__,__LINE__, and__PRETTY_FUNCTION__macros inInternalErrorwithstd::source_location::current()default parameters.std::span: Replace raw pointer + size pairs (py::object *children, ssize_t num_children) withstd::span<py::object>inMakeNodeand related functions.std::ranges: Replacestd::copy/std::reversewith iterator pairs bystd::ranges::copy/std::ranges::reversewith range arguments.requiresclauses: Replacestd::enable_if_tSFINAE with C++20requiresexpressions in template functions (TupleGetItemAs,ListGetItemAs,DictGetItemAs).std::unordered_map::contains: Replace.find() != .end()pattern with.contains().Build and CI changes
CMakeLists.txt: Raise minimum standard from C++17 to C++20.MACOSX_DEPLOYMENT_TARGET=15.0andIPHONEOS_DEPLOYMENT_TARGET=16.3(required forstd::formatsupport in Apple libc++)..clang-formatstandard toc++20andclang-tidyto use C++20.Motivation and Context
C++20 provides standard library features that simplify the codebase and improve code clarity:
std::formateliminates verbosestd::ostringstreampatterns for string formatting.std::source_locationreplaces non-portable compiler macros (__PRETTY_FUNCTION__) with a standard mechanism.std::spanprovides a safer, more expressive alternative to raw pointer + size parameters.This is a breaking change: C++20 compiler support is now required to build the extension from source.
Types of changes
Checklist
make format. (required)make lint. (required)make testpass. (required)