Skip to content

feat!: use C++20 for extension build - #235

Open
XuehaiPan wants to merge 8 commits into
mainfrom
cpp20
Open

feat!: use C++20 for extension build#235
XuehaiPan wants to merge 8 commits into
mainfrom
cpp20

Conversation

@XuehaiPan

@XuehaiPan XuehaiPan commented Aug 2, 2025

Copy link
Copy Markdown
Member

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 all std::ostringstream and operator+ string concatenation for error/exception messages with std::format. Add std::formatter specializations for py::handle and py::object.
  • std::source_location: Replace __FILE__, __LINE__, and __PRETTY_FUNCTION__ macros in InternalError with std::source_location::current() default parameters.
  • std::span: Replace raw pointer + size pairs (py::object *children, ssize_t num_children) with std::span<py::object> in MakeNode and related functions.
  • std::ranges: Replace std::copy/std::reverse with iterator pairs by std::ranges::copy/std::ranges::reverse with range arguments.
  • requires clauses: Replace std::enable_if_t SFINAE with C++20 requires expressions 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.
  • Set MACOSX_DEPLOYMENT_TARGET=15.0 and IPHONEOS_DEPLOYMENT_TARGET=16.3 (required for std::format support in Apple libc++).
  • Remove the C++17 backward-compatibility test job from CI.
  • Update .clang-format standard to c++20 and clang-tidy to use C++20.

Motivation and Context

C++20 provides standard library features that simplify the codebase and improve code clarity:

  • std::format eliminates verbose std::ostringstream patterns for string formatting.
  • std::source_location replaces non-portable compiler macros (__PRETTY_FUNCTION__) with a standard mechanism.
  • std::span provides 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

  • New feature (non-breaking change which adds core functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the CONTRIBUTION guide. (required)
  • My change requires a change to the documentation.
  • I have updated the tests accordingly. (required for a bug fix or a new feature)
  • I have updated the documentation accordingly.
  • I have reformatted the code using make format. (required)
  • I have checked the code using make lint. (required)
  • I have ensured make test pass. (required)

@XuehaiPan XuehaiPan self-assigned this Aug 2, 2025
@XuehaiPan XuehaiPan added enhancement New feature or request cxx Something related to the CXX source code labels Aug 2, 2025
@XuehaiPan
XuehaiPan marked this pull request as ready for review August 2, 2025 08:40
@XuehaiPan
XuehaiPan requested a review from Copilot August 2, 2025 08:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::ostringstream string formatting with C++20 std::format
  • Modernizes template syntax using C++20 requires clauses 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

Comment thread src/treespec/unflatten.cpp Outdated
Comment thread src/treespec/unflatten.cpp Outdated
Comment thread src/treespec/traversal.cpp Outdated
@codecov

codecov Bot commented Aug 2, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (d34f302) to head (2bdd094).
✅ All tests successful. No failed tests found.

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           
Flag Coverage Δ
unittests 100.00% <ø> (ø)
unittests-cp310-Linux 100.00% <ø> (ø)
unittests-cp310-Windows 100.00% <ø> (ø)
unittests-cp310-macOS 100.00% <ø> (ø)
unittests-cp311-Linux 100.00% <ø> (ø)
unittests-cp311-Windows 100.00% <ø> (ø)
unittests-cp311-macOS 100.00% <ø> (ø)
unittests-cp312-Linux 100.00% <ø> (ø)
unittests-cp312-Windows 100.00% <ø> (ø)
unittests-cp312-macOS 100.00% <ø> (ø)
unittests-cp313-Linux 100.00% <ø> (ø)
unittests-cp313-Windows 100.00% <ø> (ø)
unittests-cp313-macOS 100.00% <ø> (ø)
unittests-cp313t-Linux 100.00% <ø> (ø)
unittests-cp313t-Windows 100.00% <ø> (ø)
unittests-cp313t-macOS 100.00% <ø> (ø)
unittests-cp314-Linux 100.00% <ø> (ø)
unittests-cp314-Windows 100.00% <ø> (ø)
unittests-cp314-macOS 100.00% <ø> (ø)
unittests-cp314t-Linux 100.00% <ø> (ø)
unittests-cp314t-Windows 100.00% <ø> (ø)
unittests-cp314t-macOS 100.00% <ø> (ø)
unittests-cp39-Linux 100.00% <ø> (ø)
unittests-cp39-Windows 100.00% <ø> (ø)
unittests-cp39-macOS 100.00% <ø> (ø)
unittests-pp311-Linux 100.00% <ø> (ø)
unittests-pp311-Windows 100.00% <ø> (ø)
unittests-pp311-macOS 100.00% <ø> (ø)
unittests-pydebug 100.00% <ø> (ø)
unittests-pydebug-cp310d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp310d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp310d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp311d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp311d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp311d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp312d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp312d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp312d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp313d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp313d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp313d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp313td-Linux 100.00% <ø> (ø)
unittests-pydebug-cp313td-Windows 100.00% <ø> (ø)
unittests-pydebug-cp313td-macOS 100.00% <ø> (ø)
unittests-pydebug-cp314d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp314d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp314d-macOS 100.00% <ø> (ø)
unittests-pydebug-cp314td-Linux 100.00% <ø> (ø)
unittests-pydebug-cp314td-Windows 100.00% <ø> (ø)
unittests-pydebug-cp314td-macOS 100.00% <ø> (ø)
unittests-pydebug-cp39d-Linux 100.00% <ø> (ø)
unittests-pydebug-cp39d-Windows 100.00% <ø> (ø)
unittests-pydebug-cp39d-macOS 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@XuehaiPan
XuehaiPan force-pushed the cpp20 branch 17 times, most recently from 9388aa9 to b3e5183 Compare August 3, 2025 13:13
@XuehaiPan

XuehaiPan commented Aug 3, 2025

Copy link
Copy Markdown
Member Author
  • std::format support needs GCC 13 on Linux and MACOSX_DEPLOYMENT_TARGET 13.3 on macOS.
  • The default GCC on Ubuntu 22.04 LTS is GCC 11, which does not support std::format.
  • The AppleClang in the GHA macos-14 runner is 15.0.0, which has an incorrect implementation of std::source_location::current() when in the function default argument. I can build the extension with AppleClang 17.0.0 locally, which passes all tests.
  • std::source_location::current() tests passed for macOS platform with MACOSX_DEPLOYMENT_TARGET="15.0" on macos-15 runner, and iOS platform requires to set IPHONEOS_DEPLOYMENT_TARGET="16.3".

@XuehaiPan
XuehaiPan force-pushed the cpp20 branch 4 times, most recently from 6887698 to c0e6a1d Compare August 5, 2025 08:45
@XuehaiPan
XuehaiPan force-pushed the cpp20 branch 3 times, most recently from ce0ec4b to 5af5e9b Compare October 13, 2025 00:20
@XuehaiPan
XuehaiPan force-pushed the cpp20 branch 2 times, most recently from 430d021 to f35b9b6 Compare November 14, 2025 03:01
@XuehaiPan
XuehaiPan force-pushed the cpp20 branch 2 times, most recently from 74793af to 947195f Compare November 16, 2025 01:50
@XuehaiPan
XuehaiPan force-pushed the cpp20 branch 6 times, most recently from b834e96 to 4caf09f Compare November 29, 2025 10:17
@XuehaiPan
XuehaiPan force-pushed the cpp20 branch 2 times, most recently from b40c207 to 0b08991 Compare December 8, 2025 13:32
@XuehaiPan
XuehaiPan force-pushed the cpp20 branch 2 times, most recently from 84639eb to 2d05490 Compare January 28, 2026 16:30
@XuehaiPan

Copy link
Copy Markdown
Member Author

Code review

No 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 👎.

XuehaiPan added 8 commits July 3, 2026 19:26
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cxx Something related to the CXX source code enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants