Skip to content

[EXPERIMENTAL] Drop C++11 and C++14 support - #6124

Open
rwgk wants to merge 6 commits into
pybind:masterfrom
rwgk:drop_cxx11_cxx14
Open

[EXPERIMENTAL] Drop C++11 and C++14 support#6124
rwgk wants to merge 6 commits into
pybind:masterfrom
rwgk:drop_cxx11_cxx14

Conversation

@rwgk

@rwgk rwgk commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Description

EXPERIMENTAL/WIP

(Not Draft only so that the full CI runs.)

Suggested changelog entry:

  • Placeholder.

@rwgk
rwgk requested a review from henryiii as a code owner August 3, 2026 08:00
@rwgk
rwgk force-pushed the drop_cxx11_cxx14 branch from 6942ffb to 871b8a2 Compare August 3, 2026 08:58
@henryiii

henryiii commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

A few notes:

  • I want to make our next release first, then I'm personally fine with it, 17 is pretty widely supported now. I guess we'll have to find something for 11 so we can redefine what the 11 in our name means. :)
  • The interesting thing would be to see how much this changes/drops/simplifies.
  • This does raise our minimum macOS requirement on older CPythons (from memory, I think <3.12.4). That's been the reason I've avoided it so far. I think all other platforms are basically unaffected.

@rwgk

rwgk commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

A few notes:

  • I want to make our next release first,

Sounds best to me, too.

  • The interesting thing would be to see how much this changes/drops/simplifies.

I agree. But there is also the aspect that dropping C++11 and C++14 unshackles future developments. We're already at 9 years plus 17, it seems like a very long wait already. We could maintain 3.1.x (still C++11) for a couple more years, probably the effort for that will be less than what we'd pay for staying C++11 compatible on the mainline.

  • This does raise our minimum macOS requirement on older CPythons (from memory, I think <3.12.4). That's been the reason I've avoided it so far. I think all other platforms are basically unaffected.

Ah, I didn't realize. That would be another argument for actively maintaining 3.1.x until Python 3.11 reaches EOL.

@Skylion007

Copy link
Copy Markdown
Collaborator

You should ask Claude/Codex to update the Clang-Tidy rules too, a ton of opted out modernize rules can be enabled with this PR.

#ifndef PYBIND11_CPP17

template <typename T>
constexpr const char

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am more interested in how many complex templates can be removed with if constexpr

@Skylion007

Copy link
Copy Markdown
Collaborator

Also a ton of const strings ref that could be std::string_views now

@rwgk

rwgk commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Drop C++11/C++14 work notes — snapshot at 2026-08-03 10:50 PDT

This is a resumable snapshot of the experimental C++17 migration work for
pybind11 PR 6124. It consolidates the earlier roadmap and workflow-reduction
notes with the production-code work completed on 2026-08-03.

Repository and PR state

  • Repository: /wrk/forked/pybind11
  • PR: [EXPERIMENTAL] Drop C++11 and C++14 support #6124
  • PR title at the last fetch: [EXPERIMENTAL] Drop C++11 and C++14 support
  • Branch: drop_cxx11_cxx14
  • Base master when the branch was created:
    5956509
    (Delegate py::print to Python's native print (#6121))
  • Current branch HEAD:
    ec0788b
  • At this snapshot, the worktree is clean and local HEAD is exactly aligned
    with origin/drop_cxx11_cxx14.
  • Net diff against that local master: 18 files, 145 insertions, 347
    deletions (net -202 lines).

Do not assume that master, the PR discussion, or the CI state are still the
same when resuming. Fetch those first.

Working strategy

The intended process is deliberately gated:

  1. Make one coherent, reviewable step.
  2. Run focused local validation plus the default and free-threaded suites.
  3. Push that step and let the full PR CI complete.
  4. Resolve all relevant failures before stacking the next substantive step.
  5. Keep a record of deferred design decisions and compatibility machinery
    that only looks obsolete superficially.

This is partly to keep the migration safe and partly because the project has
only 20 concurrent GitHub Actions runners available.

Maintainer context

In Henry's latest PR comment before the production-code work:

  • He preferred making the next release first, but was personally comfortable
    with moving to C++17 afterward.
  • He said the interesting part was seeing how much code the change would let
    us remove or simplify. This prompted the pivot from workflow-only changes to
    the first production-code simplification pass.
  • He recalled one platform consequence: selecting C++17 can raise the minimum
    macOS deployment target for older CPython builds (roughly CPython releases
    before 3.12.4, from memory). Other platforms were thought to be largely
    unaffected. Verify and document this rather than relying on the recollection.

The experimental status remains useful: dropping the language modes and the
timing relative to the release are maintainer decisions, not conclusions that
the branch should silently encode as settled policy.

Commit sequence

Oldest first:

  1. 0cf25d2ci: replace C++11 and C++14 builds with C++17
  2. 871b8a2ci: use Clang 8 for C++17 coverage
  3. ab35e84ci: use Debian snapshot for Clang 8
  4. f6f5ae6ci: use Clang 11 for C++17 coverage
  5. bd3d48cbuild: require C++17
  6. ec0788brefactor: remove inactive pre-C++17 branches

The Clang 8 and Debian-snapshot commits record a useful diagnostic path, but
are superseded in the net tree by the Clang 11 decision. If the branch is later
rebuilt into publication-quality history, 871b8a20, ab35e84f, and
f6f5ae63 are natural squash/reword candidates. Do not rewrite them while the
experimental PR and its CI history are still useful for diagnosis.

What the workflow commits do

The workflow pass conceptually replaced retained C++11/C++14 builds with C++17
without accidentally dropping their other useful coverage:

  • All explicit low-standard configurations in the main workflows were raised
    to C++17.
  • Reusable workflows, CIBW, configure-only coverage, CUDA, installation/build
    tests, in-place builds, AppVeyor, Windows x86, and MSYS2 now establish C++17
    where necessary.
  • The duplicate Intel C++11 sequence was removed; the C++17 Intel sequence
    still runs the build, Python tests, C++ tests, CMake interface tests, and
    cross-module RTTI tests.
  • Three near-identical MSYS2 C++11/C++14/C++17 sequences became one C++17
    sequence, retaining Python, C++, interface, and RTTI coverage.
  • The upstream workflow's former C++11 Debug and C++17 default builds became
    C++17 Debug and C++17 MinSizeRel builds, preserving a meaningful second
    configuration instead of duplicating only the language standard.
  • Existing C++20/C++23, smart-holder, simple-GIL, alternate-runtime, and RTTI
    coverage remains.
  • Two stale PYBIND11_NUMPY_1_ONLY=ON settings were removed rather than
    transferred. That option is intentionally rejected by numpy.h and was
    removed in PR 5595, so preserving those flags would preserve only failures,
    not useful coverage.

Known optional workflow reductions, deliberately not taken yet:

  • High confidence: remove the Windows 2022/Python 3.10 smart-holder row.
    Windows smart-holder coverage remains on Python 3.9; /GR is the MSVC
    default and CMake supplies /EHsc.
  • Medium confidence: remove Ubuntu/Python 3.9 with PYBIND11_FINDPYTHON=ON,
    transferring that option to the Ubuntu/Python 3.12 smart-holder/simple-GIL
    row. The Python 3.9 FINDPYTHON=OFF lane would remain.
  • Lower confidence: remove macOS Intel/Python 3.13 while retaining
    Intel/Python 3.9. The 3.13 row was deliberately added in PR 5971 and survived
    the recent reduction in PR 6106, so leave it without a stronger rationale.

The original standalone note is:
/wrk/obox/drop_cxx11_cxx14_further_reductions_to_consider_2026-08-03+005433.txt.

Clang-lane investigation and final matrix decision

Promoting the old Clang 5 row from C++14 to C++17 exposed a real toolchain
limit: Clang 5 cannot compile libstdc++ 10's std::variant implementation due
to old LLVM friend/private-access bugs. The C++14 lane had selected the Boost
variant fallback, so it had hidden both that incompatibility and the lack of
explicit Boost-variant coverage in an all-C++17 matrix.

The attempted Clang 8 replacement demonstrated two further facts:

  • Clang 8 itself can compile the affected standard-variant path.
  • Its published image is based on retired Debian Buster. Even with its pinned
    snapshot repositories enabled, the coherent platform supplies CMake 3.13
    and Python 3.7, below pybind11's current CMake 3.15 and Python 3.9 floors.

The final matrix therefore uses:

  • Clang 11 / C++17 on Bullseye
  • Clang 16 / C++20
  • Clang 18 / C++20

This records Clang 11 as the oldest continuously tested LLVM toolchain. It does
not by itself declare Clang 11 to be a formal compiler-support floor.

The full workflow-only CI gate for f6f5ae63 completed successfully before
the production changes began (run 30803392874; 75 successful and 2 skipped
checks in the view inspected at the time).

C++17 contract commit: bd3d48c9

This commit establishes the policy contract before deleting inactive code:

  • include/pybind11/detail/common.h emits a clear error below C++17.
  • pybind11_headers advertises CMake compile feature cxx_std_17, allowing a
    linked consumer target to be promoted automatically.
  • The default CMake preset requests C++17; AppVeyor and the cross-module RTTI
    test library were updated accordingly.
  • The deprecated PYBIND11_CPP_STANDARD path accepts only 17 or 20 and rejects
    11/14 with a minimum-version diagnostic.
  • pybind11/setup_helpers.py now:
    • describes C++17+ projects;
    • rejects explicit integer levels 11 and 14;
    • probes only C++17 when automatically selecting a level;
    • reports C++17 as the minimum;
    • removes the obsolete Windows 11-to-14 conversion; and
    • uses the macOS 10.14 deployment-target logic appropriate for C++17.
  • Installed-target and subdirectory-target CMake tests deliberately initialize
    consumers with CXX_STANDARD 14 and verify that the pybind11 interface raises
    the effective standard to C++17. These C++14 settings are intentional tests,
    not leftovers to remove.

One policy edge found after pushing:

  • The setup helper rejects integer 11 and 14, but its broadened
    str | int | None interface permits strings such as "11" and "14".
    The header still rejects the eventual build, so this is not a silent
    correctness hole. Decide whether the helper should reject numeric strings
    early or constrain strings to the Windows sentinel "latest".

First production simplification: ec0788b3

This pass was intentionally mechanical: retain the already-active C++17 path
and delete branches that became unreachable after bd3d48c9.

It changes four headers by 24 insertions and 169 deletions (net -145):

  • detail/common.h
    • uses standard C++ type aliases directly;
    • removes the custom index-sequence implementation;
    • makes [[deprecated]], [[maybe_unused]], and overload_cast
      unconditional (while retaining the MinGW deprecation exception);
    • removes pre-C++17 arithmetic format_descriptor definitions;
    • simplifies standard-library feature gates now that C++17 is mandatory;
    • raises the classic Intel compiler diagnostic from v18 to v19, preserving
      the effective behavior of the former C++17 path.
  • detail/init.h
    • keeps generalized lambda move captures and removes three C++11 capture
      fallbacks in constructor and pickle factories.
  • pybind11.h
    • uses if constexpr directly;
    • removes the PYBIND11_MAYBE_CONSTEXPR compatibility macro;
    • removes inactive pre-C++17 out-of-line static-member definitions; and
    • updates the main-header description from C++11 to C++17.
  • complex.h
    • removes the inactive pre-C++17 out-of-line format_descriptor definition.

The commit does not change internals layout, holder layout, conduit ABI,
platform ABI construction, or the intended active implementation for an
already-supported C++17 build.

Validation completed for the two newest commits

Focused checks:

  • A direct C++17 include/compile check succeeds.
  • The equivalent C++14 compile fails at the intended
    pybind11 requires C++17 or newer diagnostic.
  • A CMake subdirectory consumer initially requesting C++14 builds and imports
    successfully after pybind11::module promotes it to C++17.
  • The complete pre-commit suite passes.

Requested full local validation:

cd /wrk/bld/pybind11_gcc_v3.14.4_23116f998f6_default
scons -j $(nproc) && TestVenv/bin/python3 ../../clone/pybind11_scons/run_tests.py ../../forked/pybind11 24

cd /wrk/bld/pybind11_gcc_v3.14.4_23116f998f6_freethreaded
scons -j $(nproc) && TestVenv/bin/python3 ../../clone/pybind11_scons/run_tests.py ../../forked/pybind11 24

Both the default and free-threaded suites passed.

The focused tests/extra_setuptools/test_setuphelper.py invocation could not
run in these local TestVenvs because they contain neither setuptools nor
the removed stdlib distutils. The failures occurred during test imports,
before reaching the changed behavior. Static/type/lint checks passed, but the
runtime setup-helper paths need the post-push CI result or a packaging-capable
environment for confirmation.

Pushing bd3d48c9 and ec0788b3 triggered a new PR CI run. Per the explicit
request, that run was not queried or monitored. Its state is unknown in this
snapshot and should be the first thing checked when work resumes.

Deliberate exceptions and compatibility machinery to preserve

Do not mechanically remove the following merely because the core library now
requires C++17:

  • PYBIND11_CPP14, PYBIND11_CPP17, and PYBIND11_CPP20: downstream code may
    use these public-ish feature probes. PYBIND11_CPP17 also enforces the floor.
  • include/pybind11/conduit/pybind11_conduit_v1.h: this is intentionally a
    tiny standalone cross-framework protocol header, dependent only on
    Python.h, and should remain ISO C++11-compatible unless that separate
    portability contract is explicitly reconsidered.
  • Experimental filesystem/optional fallbacks and standard-library feature
    detection: C++17 language mode does not guarantee a complete C++17 library.
  • __cpp_noexcept_function_type and fold-expression capability guards, the
    Intel fold-expression workaround, and the PGI/Intel void_t split: retained
    compiler frontends may be partially conforming.
  • PYBIND11_STD_LAUNDER: detection currently occurs before <new> necessarily
    defines the library feature macro. Making std::launder unconditional is
    low-level object-lifetime work and should be isolated.
  • weak_from_this fallback logic: a C++17 rewrite changes holder/ownership
    behavior and needs dedicated shared-pointer and smart-holder validation.
  • Aligned and sized allocation guards: language mode alone is insufficient;
    allocation/deallocation mismatches are memory-safety bugs.
  • variant_caster_visitor::result_type: older Boost.Variant can require this
    even when the consumer language mode is C++17.
  • The functional.h wrapper: it is not just a move-capture backport; it makes
    std::function copyable while acquiring the GIL during Python-function copy
    and destruction.
  • argument_vector's tagged union: it is intentionally smaller than
    std::variant, preserving an extra inline handle. This is a layout/performance
    choice, not just old-language syntax.
  • Address-observable static constexpr objects such as overload_cast and
    const_: do not convert them wholesale to namespace-level inline constexpr
    without considering cross-translation-unit address identity.
  • Old compiler-version checks: many are effectively dominated by the C++17
    gate, but deleting them should follow an explicit compiler-support decision,
    not be presented as language-mode dead-code cleanup.

Do not change detail::internals, holder layouts, function_record, conduit
identifiers, or platform ABI construction as part of this cleanup. An internals
layout change requires an internals-version decision; a function_record
layout change requires its ABI-ID decision.

Guardrails still missing

These were planned before changing the policy contract, but have not yet been
implemented:

  1. Add an explicit Boost-variant caster test under C++17, with bindings distinct
    from the standard-variant test. The old C++11/C++14 matrix selected Boost as
    a fallback; an all-C++17 matrix now selects std::variant, so current CI no
    longer proves the Boost customization path.
  2. Add a standalone compile-only check for
    conduit/pybind11_conduit_v1.h, explicitly using C++11 and disabled
    exceptions without linking pybind11::headers. This protects the one
    intentional C++11 contract from future core-header modernization.

These are strong candidates to add before more ambitious active-code rewrites.

Good candidates for the next small simplification step

Low-risk production cleanup:

  • In detail/common.h, replace local C++11-era bool_constant and negation
    backports with std::bool_constant and std::negation aliases.
  • In iostream.h, replace the ternary used to avoid a pre-C++17
    static-constexpr ODR-use with std::max(buffer_size, minimum_buffer_size);
    <algorithm> is already present.
  • Correct stale production comments without deleting semantically important
    code:
    • explain the real GIL/copyability role of the functional.h wrapper;
    • explain the compact-layout rationale for argument_vector without saying
      C++11/C++14 are supported; and
    • say Boost compatibility, rather than “Boost in C++11,” for the variant
      visitor result_type.

Safe test-only cleanup, preferably in its own commit:

  • Remove now-dead PYBIND11_OVERLOAD_CAST fallback branches in
    test_methods_and_attributes.cpp and test_constants_and_functions.cpp.
  • Remove unreachable C++11/C++14 reporting from tests/pybind11_tests.cpp.
  • Remove the now-redundant PYBIND11_CPP17 test branch in test_class.cpp.
  • Replace the generic-lambda compatibility macros in
    test_with_catch/test_argument_vector.cpp and
    test_args_convert_vector.cpp with direct generic lambdas.
  • Update stale C++11/C++14 comments in test_stl.cpp,
    test_methods_and_attributes.cpp, and test_class.py.

Higher-risk or policy-dependent work should remain separate:

  • Collapsing fold-expression, noexcept function-type, void_t, optional,
    filesystem, or compiler-bug branches.
  • Rewriting ReadableFunctionSignature around deduced return types.
  • Converting broad trait usage to _v, std::invoke, inline variables, or
    other “idiomatic C++17” forms. These can affect SFINAE, instantiation timing,
    overload resolution, diagnostics, linkage, or address identity.
  • Ownership, smart-holder, and internals modernization; do these last and with
    mixed-module/previous-header ABI validation where appropriate.

Documentation and metadata still to update

Some policy-facing text was updated in the setup helper and main-header
description, but the public migration/documentation pass remains incomplete.

Current non-historical items include:

  • pyproject.toml project description/classifiers
  • tools/pybind11.pc.in and its exact-value test in
    tests/extra_python_package/test_files.py
  • current build examples and minimum-standard guidance in docs/compiling.rst
  • current guidance in docs/basics.rst, docs/classes.rst,
    docs/advanced/deprecated.rst, docs/advanced/cast/stl.rst, the FAQ, and
    benchmark material
  • README supported-platform/compiler wording
  • the current release changelog and upgrade guidance
  • repository/agent instructions that still describe the default as following
    the consumer toolchain without mentioning the C++17 floor

Prefer a future-proof branding phrase such as “C++ and Python,” with C++17
stated separately as a requirement. Do not add -std=c++17 to pybind11.pc:
flag ordering could override a consumer's newer standard. Preserve historical
C++11/C++14 references in old changelog and upgrade entries, factual language
feature descriptions, and the C++14 settings in the two CMake promotion tests.

Suggested resume checklist

  1. Confirm this branch still points at ec0788b or understand all later
    commits before using this note.
  2. Fetch the PR discussion and inspect the full CI run triggered by
    ec0788b; do not rely only on the aggregate check count.
  3. Resolve CI or review feedback before starting another cleanup cluster.
  4. Revisit the setup-helper numeric-string edge.
  5. Decide whether the next gate should be the missing Boost/conduit guardrails
    or the small production/test cleanup listed above.
  6. Keep capability/compiler workarounds out of a mechanical-deletion commit.
  7. Run focused tests, then the default and free-threaded commands above.
  8. Push only that coherent step and wait for its full CI gate.
  9. Update or supersede this snapshot with new SHAs, CI observations, and
    decisions.

Earlier detailed roadmap retained for history:
/wrk/obox/pr6124_cxx17_migration_roadmap_and_progress.md.

@rwgk

rwgk commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

@rwgk

rwgk commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

You should ask Claude/Codex to update the Clang-Tidy rules too, a ton of opted out modernize rules can be enabled with this PR.

Hi @Skylion007, I won't be able to work on this for a while, but I just added a couple comments that should make it possible for you (or anyone) to pick up where I left off:

The idea is that you can feed this PR to your favorite agent/model to continue. Please feel free to add commits, but please don't rebase or force push. Simply keep merging and adding commits, that's something an agent (or me) can easily follow.

@henryiii

Copy link
Copy Markdown
Collaborator

Spiel: By the way, I strongly, strongly recommend rebasing for a long-lived branch like this. If the stack goes in first (which I'd really like for it to!), then this will likely make no sense with a merge comment. Rebasing will make every commit continue to make sense and continue to tell a story. Agents, like humans, work better with rebasing; linear history is much, much simpler. Also, by the way, GitHub's new stacks feature does not support merges, they require always rebasing.

Rebases preserve the original story, but applied to the current codebase.

I've seen a long-lived PR die (in pypa/build) because there was no way to recover it after a dozen merge commits.

Also, as I've pointed out before, every past commit remains accessible in git after a force push.


Anyway, I'm thinking I can pull out the C++11 drop and see what that looks like on its own. There's no drawback at all going C++14, while there's a tiny bit going C++17, and I'm curious to see what it looks like.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants