From d67bf32f8a7b5fea35ac236978cfc854df8c5981 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 4 Sep 2026 12:03:08 -0400 Subject: [PATCH 1/4] ce: publish flattened headers for Compiler Explorer Compiler Explorer can include a header from a URL, but only one file at a time: it does not resolve the includes inside the file it fetches. The scripts that produced single-file headers for the review went away with the old main.yml in 83c8ab9 ("CI: adopt boost-cli GHA"), and what was left in dev/ had gone stale - flatten.sh still named compiler.hpp, shared_ptr.hpp and unique_ptr.hpp. dev/flatten.py now rewrites *every* public header into a self-sufficient file, so a CE example's include list matches a local one line for line: #include #include boost/openmethod.hpp is the root and carries its whole closure. Every other header carries only what the root does not provide - its detail/ headers, and interop/virtual_any.hpp - and turns the rest into a check on the include guard, so that using one without the root fails on a single #error instead of a wall of undeclared identifiers. Guard names are read from the header being flattened: several are legacy and do not match their path. The antora job regenerates them into the Pages artifact, at the root of the site, next to the documentation. CE fetches URL includes client-side, so the host must send CORS headers; GitHub Pages does. dev/check-flat.sh, run by the new flat-headers job, compiles each generated header after the root, checks that each one fails on its own, and builds and runs the ce/ examples against the generated tree. Those examples are now part of the CMake build too - ce/CMakeLists.txt was referenced from nowhere and carried mangled identifiers from a bad sed, and two of the sources had been missing `using boost::openmethod::virtual_;` long enough to stop compiling. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011HmyQexbZAWZk6BDkHSbdQ --- .github/workflows/ci.yml | 29 +++++++ .gitignore | 3 + CLAUDE.md | 40 +++++++++ CMakeLists.txt | 2 + ce/2-method.cpp | 2 + ce/CMakeLists.txt | 42 ++++------ ce/README.md | 50 ++++++----- ce/uni-method.cpp | 2 + dev/check-flat.sh | 96 +++++++++++++++++++++ dev/flatten.py | 175 ++++++++++++++++++++++++++++++++------- dev/flatten.sh | 11 --- dev/local-flat.sh | 12 --- 12 files changed, 364 insertions(+), 100 deletions(-) create mode 100755 dev/check-flat.sh mode change 100644 => 100755 dev/flatten.py delete mode 100755 dev/flatten.sh delete mode 100755 dev/local-flat.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 705ad446..89fc6c6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,6 +97,29 @@ jobs: cmake --build ../build --target tests -j $(nproc) ctest --test-dir ../build -j $(nproc) --output-on-failure + flat-headers: + name: Flat headers + runs-on: ubuntu-24.04 + steps: + - name: Clone Boost.OpenMethod + uses: actions/checkout@v4 + + - name: Clone Boost + uses: alandefreitas/cpp-actions/boost-clone@v1.8.8 + with: + branch: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }} + boost-dir: ../boost-source + scan-modules-dir: . + scan-modules-ignore: openmethod + + # Each flattened header must compile after boost/openmethod.hpp, and fail + # with our own diagnostic without it; the ce/ examples must build and run + # against the generated tree. + - name: Check the flat headers + env: + BOOST_SRC_DIR: ${{ github.workspace }}/../boost-source + run: dev/check-flat.sh + antora: name: Antora docs strategy: @@ -156,6 +179,12 @@ jobs: exit 1 fi + # Published alongside the docs, at the root of the site, so that a + # Compiler Explorer example can include them by URL. See ce/README.md. + - name: Build flat headers + if: matrix.os == 'ubuntu-latest' + run: python3 dev/flatten.py --output-dir doc/html + - name: Create Antora docs artifact uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index cae127aa..87dd85ca 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ cov-int/** # Personal/developer scratch files, not project content. .claude/settings.json notes.txt + +# Flattened headers for Compiler Explorer (dev/flatten.py). +flat/ diff --git a/CLAUDE.md b/CLAUDE.md index 690de73e..3bcfc180 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -491,6 +491,46 @@ carries the override on the file's behalf (`test_capture_errors.hpp`). Add anoth and the scan has to learn about it: miss one and the file still compiles, binds to `default_registry`, and fails at run time. +### Flattened headers for Compiler Explorer + +`dev/flatten.py` rewrites every public header into a self-sufficient file under `flat/`; the +`antora` CI job regenerates them into the Pages artifact, so they are served from the root of +`https://jll63.github.io/openmethod` and a CE example can include them by URL. CE fetches those +includes client-side, which is why the host has to send CORS headers - GitHub Pages does, +`access-control-allow-origin: *`. + +The point of the exercise is that a CE example's include list matches a local one line for line: + +```cpp +#include +#include +``` + +`boost/openmethod.hpp` is the root and carries its whole closure. **Every other header carries +only what the root does not provide** - its `detail/` headers, and `interop/virtual_any.hpp`, +which nothing includes directly. A dependency the root *does* provide becomes a guard check: + +```cpp +#ifndef BOOST_OPENMETHOD_CORE_HPP +#error ": #include <.../boost/openmethod.hpp> first" +#endif +``` + +so a missing root fails on one line instead of a wall of undeclared identifiers. Guard names are +read from the header being flattened, never hardcoded - several are legacy and do not match their +path (`initialize.hpp` is `BOOST_OPENMETHOD_COMPILER_HPP`, `preamble.hpp` is +`BOOST_OPENMETHOD_REGISTRY_HPP`, `policies/static_rtti.hpp` is +`BOOST_OPENMETHOD_POLICY_MINIMAL_RTTI_HPP`). + +The rewriting is line-oriented, which holds only because no `#include ` in +the tree sits inside an `#if`. A `//!` doc comment containing one is left alone - the regex is +anchored at the start of the line. + +`dev/check-flat.sh` (the `flat-headers` CI job, and `BOOST_SRC_DIR=... dev/check-flat.sh` locally) +compiles each generated header after the root, checks that each one *fails* on its own, and builds +and runs the `ce/*.cpp` examples against the generated tree. Those examples are also ordinary +tests in the CMake build (`ce/CMakeLists.txt`), so they cannot rot silently. + ### Custom RTTI When `` is unavailable or insufficient, use static_rtti or implement custom RTTI. See `doc/modules/ROOT/examples/custom_rtti/` and policies in `include/boost/openmethod/policies/`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f119dfd..47ea21b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -325,5 +325,7 @@ if (BOOST_OPENMETHOD_BUILD_TESTS) add_subdirectory(doc/modules/ROOT/examples) # Sources behind the `include:` markers in the reference doc comments. add_subdirectory(doc/modules/ROOT/snippets) + # Examples published on Compiler Explorer, see ce/README.md. + add_subdirectory(ce) endif () endif () diff --git a/ce/2-method.cpp b/ce/2-method.cpp index 4be0a11d..59f9d2cb 100644 --- a/ce/2-method.cpp +++ b/ce/2-method.cpp @@ -21,6 +21,8 @@ struct Cat : Animal { BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); +using boost::openmethod::virtual_; + BOOST_OPENMETHOD( meet, (virtual_, virtual_, std::ostream&), void); diff --git a/ce/CMakeLists.txt b/ce/CMakeLists.txt index 60739a87..6636593e 100644 --- a/ce/CMakeLists.txt +++ b/ce/CMakeLists.txt @@ -1,28 +1,18 @@ # Copyright (c) 2017-2026 Jean-Louis Leroy # Distributed under the Boost Software License, Version 1.0. -# See accompanying filce_e LICENSE_1_0.txt -# or copy at hce_ttp://www.boost.oce_rg/LICENSE_1_0.txt) - -add_executable(ce_virtual virtual.cpp) -add_test(NAME ce_virtual COMMAND ce_virtual) - -add_executable(ce_uni-method uni-method.cpp) -add_test(NAME ce_uni-method COMMAND ce_uni-method) - -add_executable(ce_uni-method-vptr uni-method-vptr.cpp) -add_test(NAME ce_uni-method-vptr COMMAND ce_uni-method-vptr) - -add_executable(ce_virtual-double virtual-double.cpp) -add_test(NAME ce_virtual-double COMMAND ce_virtual-double) - -add_executable(ce_2-method 2-method.cpp) -add_test(NAME ce_2-method COMMAND ce_2-method) - -add_executable(ce_2-method-vptr 2-method-vptr.cpp) -add_test(NAME ce_2-method-vptr COMMAND ce_2-method-vptr) - -add_executable(ce_2-method-vptr-final 2-method-vptr-final.cpp) -add_test(NAME ce_2-method-vptr-fince_al COMMAND ce_2-method-vptr-final) - -add_executable(ce_uni-method-vptr-final uni-method-vptr-final.cpp) -add_test(NAME ce_uni-method-vptr-fce_inal COMMAND ce_uni-method-vptr-final) +# See accompanying file LICENSE_1_0.txt +# or copy at http://www.boost.org/LICENSE_1_0.txt) + +message(STATUS "Boost.OpenMethod: building Compiler Explorer examples") + +file(GLOB cpp_files "*.cpp") + +foreach (cpp ${cpp_files}) + get_filename_component(stem ${cpp} NAME_WE) + set(test_target "boost_openmethod-ce-${stem}") + add_executable(${test_target} ${cpp}) + boost_openmethod_enable_reflection(${test_target}) + target_link_libraries(${test_target} PRIVATE Boost::openmethod) + add_test(NAME ${test_target} COMMAND ${test_target}) + add_dependencies(tests ${test_target}) +endforeach() diff --git a/ce/README.md b/ce/README.md index 46c9a73c..a98b2039 100644 --- a/ce/README.md +++ b/ce/README.md @@ -1,26 +1,38 @@ -# YOMM2 on Compiler Explorer +# Boost.OpenMethod on Compiler Explorer -YOMM2 is available on Compiler Explorer. Make sure that you also select Boost -version 1.74 or above, and you probably want to add the `-O3 -DNDEBUG` compiler -switches. +Compiler Explorer can include a header from a URL, but only one file at a time: +it does not resolve the includes inside the file it fetches. `dev/flatten.py` +thus rewrites each public header into a self-sufficient one, and CI publishes +them at the root of , next to the +documentation. -The following examples are available: +An example on Compiler Explorer therefore includes exactly what it would +include locally, one line per header, in the same order: -* The [examples](https://jll63.github.io/yomm2/ce/slides.html) from the slides. -* The matrix example from the GitHub langing page. +```cpp +#include +#include +``` -The following examples use the diff mode to compare open methods with the -equivalent (closed) virtual function based approaches. +Every path under `include/boost/` is available under that URL - the interops, +the policies, `inplace_vptr.hpp`. Two things to know: -* [virtual function call vs uni-method call via plain reference](https://jll63.github.io/yomm2/ce/vf-vs-1m-ref.html) -* [virtual function call vs uni-method call via virtual_ptr ](https://jll63.github.io/yomm2/ce/vf-vs-1m-vptr.html) -* [double dispatch vs multi-method call via plain reference](https://jll63.github.io/yomm2/ce/2d-vs-2m-ref.html) -* [double dispatch vs multi-method call via virtual_ptr ](https://jll63.github.io/yomm2/ce/2d-vs-2m-vptr.html) +* `boost/openmethod.hpp` comes first. It is the only self-contained file; the + others check that it has been included and stop with an `#error` otherwise. +* Select a Boost version in the *Libraries* dropdown. The flattened headers + still include Boost.Mp11, Boost.DynamicBitset and the rest from Boost itself. -YOMM2 can also [add polymorphic operations to non-polymorphic -classes](https://jll63.github.io/yomm2/ce/vptr-final.html). +`-std=c++17 -O3 -DNDEBUG` is a good set of options to look at the generated +code. -When `virtual_ptr` is used in combination with generated static offsets, method -dispatch matches the speed of virtual functions. It is also possible to generate -dispatch data that can be installed without calling `update`, a fairly expensive -operaiton. See [this example](https://jll63.github.io/yomm2/ce/generator.html). +The sources in this directory are the examples published on Compiler Explorer. +They are built and run as part of the test suite, and again against the +flattened headers by `dev/check-flat.sh`, so a broken flattening is caught +before it reaches the site. + +To generate the headers locally: + +```bash +python3 dev/flatten.py # writes flat/boost/... +BOOST_SRC_DIR=/path/to/boost dev/check-flat.sh +``` diff --git a/ce/uni-method.cpp b/ce/uni-method.cpp index 0ce2675a..c4e24738 100644 --- a/ce/uni-method.cpp +++ b/ce/uni-method.cpp @@ -21,6 +21,8 @@ struct Cat : Animal { BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat); +using boost::openmethod::virtual_; + BOOST_OPENMETHOD(poke, (virtual_, std::ostream&), void); BOOST_OPENMETHOD_OVERRIDE(poke, (Cat & animal, std::ostream& os), void) { diff --git a/dev/check-flat.sh b/dev/check-flat.sh new file mode 100755 index 00000000..149f43c0 --- /dev/null +++ b/dev/check-flat.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Copyright (c) 2017-2026 Jean-Louis Leroy +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt +# or copy at http://www.boost.org/LICENSE_1_0.txt) + +# Check the headers generated by dev/flatten.py: each one must compile after the +# root, fail with our own diagnostic without it, and the ce/ examples must build +# and run against the generated tree. +# +# Usage: BOOST_SRC_DIR=/path/to/boost dev/check-flat.sh [output-dir] + +set -uo pipefail + +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +flat_dir="${1:-$root_dir/flat}" +boost_dir="${BOOST_SRC_DIR:-$root_dir/../..}" +cxx="${CXX:-g++}" +cxxstd="${CXXSTD:--std=c++17}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +# A modular tree (libs/*/include) is what CI clones; a monolithic one has the +# headers under boost/. Prefer the former: the latter can be a stale b2 headers +# tree. +if [ -d "$boost_dir/libs" ]; then + boost_includes=() + for dir in "$boost_dir"/libs/*/include "$boost_dir"/libs/numeric/*/include; do + [ -d "$dir" ] && boost_includes+=("-I$dir") + done +else + boost_includes=("-I$boost_dir") +fi + +python3 "$root_dir/dev/flatten.py" \ + --include-dir "$root_dir/include" --output-dir "$flat_dir" > /dev/null || exit 1 + +compile=("$cxx" "$cxxstd" "-I$flat_dir" "${boost_includes[@]}") +status=0 + +report() { + printf '%-6s %s\n' "$1" "$2" + [ "$1" = ok ] || status=1 +} + +cd "$flat_dir" || exit 1 + +for header in $(find boost -name '*.hpp' | sort); do + if [ "$header" = boost/openmethod.hpp ]; then + printf '#include <%s>\n' "$header" > "$work/tu.cpp" + else + printf '#include \n#include <%s>\n' "$header" \ + > "$work/tu.cpp" + fi + + if "${compile[@]}" -fsyntax-only "$work/tu.cpp" > "$work/log" 2>&1; then + report ok "$header" + else + report FAIL "$header" + head -20 "$work/log" + fi + + grep -q '^#error' "$header" || continue + + # Without the root, the guard checks must fire. + printf '#include <%s>\n' "$header" > "$work/tu.cpp" + + if "${compile[@]}" -fsyntax-only "$work/tu.cpp" > "$work/log" 2>&1; then + report FAIL "$header (alone: expected an error)" + elif grep -q 'openmethod.hpp> first' "$work/log"; then + report ok "$header (alone)" + else + report FAIL "$header (alone: not our diagnostic)" + head -20 "$work/log" + fi +done + +cd "$root_dir" || exit 1 + +for source in ce/*.cpp; do + example="$work/$(basename "$source" .cpp)" + + if ! "${compile[@]}" -O2 -o "$example" "$source" > "$work/log" 2>&1; then + report FAIL "$source" + head -20 "$work/log" + continue + fi + + if "$example" > /dev/null; then + report ok "$source" + else + report FAIL "$source (run)" + fi +done + +exit $status diff --git a/dev/flatten.py b/dev/flatten.py old mode 100644 new mode 100755 index 01c98055..e2b70027 --- a/dev/flatten.py +++ b/dev/flatten.py @@ -1,47 +1,158 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 +# Copyright (c) 2017-2026 Jean-Louis Leroy +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt +# or copy at http://www.boost.org/LICENSE_1_0.txt) + +"""Generate flattened copies of the public headers, for Compiler Explorer. + +CE can include a header from a URL, but only one file at a time: it does not +resolve the includes inside the file it fetches. Each public header thus becomes +a self-sufficient file, so that a CE example includes exactly what a local +example includes, one line per header: + + #include + #include + +`boost/openmethod.hpp` is the root: it carries its entire closure. Every other +header carries only what the root does not already provide - its `detail/` +headers, `interop/virtual_any.hpp` - and replaces the rest with a check on the +include guard, so that including it without the root fails with a diagnostic +instead of a wall of undeclared identifiers. +""" import argparse -from pathlib import Path import re +import subprocess +from pathlib import Path -parser = argparse.ArgumentParser() -parser.add_argument("output", type=Path) -parser.add_argument("input", nargs="+", type=Path) -args = parser.parse_args() +ROOT = "boost/openmethod.hpp" +DETAIL = "boost/openmethod/detail/" +INCLUDE = re.compile(r"#include <(boost/openmethod(?:/[^>]+)?\.hpp)>") +GUARD = re.compile(r"#ifndef (\w+)$") -prefix = args.input[0].absolute() -while prefix.name != "boost": - assert prefix.parent != prefix - prefix = prefix.parent +class Flattener: + def __init__(self, include_dir, base_url, revision): + self.include_dir = include_dir + self.base_url = base_url.rstrip("/") + self.revision = revision + # What the root already brings in, minus the `detail/` headers, which + # are small and are copied into every header that needs them. + self.provided = { + header + for header in self.dependencies(ROOT) + if not header.startswith(DETAIL) + } -prefix = prefix.parent -skip = len(str(prefix)) + 1 + def read(self, header): + return (self.include_dir / header).read_text() + def guard_of(self, header): + lines = self.read(header).splitlines() -def flatten(input, output, done): - header = str(input)[skip:] - if header in done: - return - done.add(header) - with input.open() as ifh: - for line in ifh: - if input.name != "openmethod.hpp" and re.match( - r"#include <(boost/openmethod/core\.hpp+)>", line - ): - continue + for line, next_line in zip(lines, lines[1:]): + if (m := GUARD.match(line)) and next_line == f"#define {m[1]}": + return m[1] + + raise SystemExit(f"{header}: no include guard") + + def dependencies(self, header, found=None): + found = set() if found is None else found + + for dep in INCLUDE.findall(self.read(header)): + if dep not in found: + found.add(dep) + self.dependencies(dep, found) + + return found + + def headers(self): + return [ROOT] + sorted( + header + for path in (self.include_dir / "boost/openmethod").rglob("*.hpp") + if not ( + header := path.relative_to(self.include_dir).as_posix() + ).startswith(DETAIL) + ) + + def write(self, header, output): + if header == ROOT: + note = "// This file is self-contained.\n" + else: + note = f"// #include <{self.base_url}/{ROOT}> first.\n" + + output.write( + f"// <{header}>, flattened for Compiler Explorer.\n" + f"{note}" + "//\n" + "// Generated by dev/flatten.py from Boost.OpenMethod" + f" {self.revision}. Do not edit.\n" + "// See https://github.com/boostorg/openmethod\n\n" + ) + + # The root carries everything; the others require what it provides. + provided = set() if header == ROOT else self.provided + self.copy(header, output, provided, header, set()) + + def copy(self, header, output, provided, top, done): + if header in done: + return + + done.add(header) + + for line in self.read(header).splitlines(keepends=True): + if m := INCLUDE.match(line): + dep = m[1] + + if dep in provided: + output.write( + f"#ifndef {self.guard_of(dep)}\n" + f'#error "<{top}>:' + f" #include <{self.base_url}/{ROOT}> first\"\n" + "#endif\n" + ) + else: + output.write("\n") + self.copy(dep, output, provided, top, done) + output.write("\n") - if m := re.match(r"#include <(boost/openmethod/[^>]+)>", line): - include = m[1] - print(file=output) - flatten(prefix / include, output, done) - print(file=output) continue output.write(line) -with args.output.open("w") as ofh: - done = set() - for input in args.input: - flatten(input.absolute(), ofh, done) +def revision(): + try: + return subprocess.run( + ["git", "describe", "--always", "--dirty"], + capture_output=True, + check=True, + text=True, + ).stdout.strip() + except (OSError, subprocess.CalledProcessError): + return "(unknown revision)" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--include-dir", type=Path, default=Path("include")) + parser.add_argument("--output-dir", type=Path, default=Path("flat")) + parser.add_argument( + "--base-url", default="https://jll63.github.io/openmethod" + ) + args = parser.parse_args() + + flattener = Flattener(args.include_dir, args.base_url, revision()) + + for header in flattener.headers(): + path = args.output_dir / header + path.parent.mkdir(parents=True, exist_ok=True) + + with path.open("w") as output: + flattener.write(header, output) + + print(path) + + +main() diff --git a/dev/flatten.sh b/dev/flatten.sh deleted file mode 100755 index cef33e6d..00000000 --- a/dev/flatten.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -mkdir -p build_outputs_folder/boost/openmethod - -python3 dev/flatten.py build_outputs_folder/boost/openmethod.hpp \ - include/boost/openmethod.hpp - -for header in core compiler shared_ptr unique_ptr; do - python3 dev/flatten.py "build_outputs_folder/boost/openmethod/$header.hpp" \ - "include/boost/openmethod/$header.hpp" -done diff --git a/dev/local-flat.sh b/dev/local-flat.sh deleted file mode 100755 index 33ea9172..00000000 --- a/dev/local-flat.sh +++ /dev/null @@ -1,12 +0,0 @@ -mkdir -p flat/boost/openmethod -python3 dev/flatten.py \ - flat/boost/openmethod.hpp \ - include/boost/openmethod.hpp \ - include/boost/openmethod/interop/std_unique_ptr.hpp \ - include/boost/openmethod/interop/std_shared_ptr.hpp \ - include/boost/openmethod/initialize.hpp -python3 dev/flatten.py \ - flat/boost/openmethod/registry.hpp \ - include/boost/openmethod/registry.hpp \ - include/boost/openmethod/policies/*.hpp \ - include/boost/openmethod/default_registry.hpp From 2a0dedad68e1ecce5be4ee769e5b7e87a86e24cf Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 4 Sep 2026 12:41:12 -0400 Subject: [PATCH 2/4] ce: add a reflection example Registers Animal, Dog, Cat and Bulldog by C++26 reflection - one BOOST_OPENMETHOD_REGISTER_CLASSES() instead of BOOST_OPENMETHOD_CLASSES. Bulldog is what the example is about: no overrider mentions it and no class list names it, yet the scan finds it deriving from Dog, registers it, and poke dispatches it to Dog's overrider. It needs a compiler that implements P2996 - x86-64 gcc (trunk) on Compiler Explorer, with -std=c++26 -freflection - and has no C++17 fallback, so ce/CMakeLists.txt builds it only when BOOST_OPENMETHOD_ENABLE_REFLECTION is on, and dev/check-flat.sh probes for such a compiler (CXX_REFLECTION, $CXX, then g++-16) and reports the example as skipped when there is none. The flat-headers job installs GCC 16 so that CI compiles it rather than skips it. Checked against the deployed headers the way CE consumes them: fetch boost/openmethod.hpp and boost/openmethod/initialize.hpp from the Pages site, paste each in place of its #include line, compile the result with g++-16 -std=c++26 -freflection -O3 -DNDEBUG. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011HmyQexbZAWZk6BDkHSbdQ --- .github/workflows/ci.yml | 8 ++++++ ce/CMakeLists.txt | 7 +++++ ce/README.md | 14 ++++++++++ ce/reflection.cpp | 57 ++++++++++++++++++++++++++++++++++++++++ dev/check-flat.sh | 57 ++++++++++++++++++++++++++++++++++++++-- 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 ce/reflection.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89fc6c6f..48e1b966 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,14 @@ jobs: name: Flat headers runs-on: ubuntu-24.04 steps: + # ce/reflection.cpp needs C++26 reflection; dev/check-flat.sh finds this + # compiler on its own, and reports the example as skipped without it. + - name: Install GCC 16 + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update + sudo apt-get install -y g++-16 + - name: Clone Boost.OpenMethod uses: actions/checkout@v4 diff --git a/ce/CMakeLists.txt b/ce/CMakeLists.txt index 6636593e..039b772c 100644 --- a/ce/CMakeLists.txt +++ b/ce/CMakeLists.txt @@ -7,6 +7,13 @@ message(STATUS "Boost.OpenMethod: building Compiler Explorer examples") file(GLOB cpp_files "*.cpp") +# reflection.cpp registers its classes by reflection, and has no fallback: +# BOOST_OPENMETHOD_REGISTER_CLASSES expands to nothing without C++26 reflection, +# and no class is registered at all. +if (NOT BOOST_OPENMETHOD_ENABLE_REFLECTION) + list(REMOVE_ITEM cpp_files "${CMAKE_CURRENT_SOURCE_DIR}/reflection.cpp") +endif() + foreach (cpp ${cpp_files}) get_filename_component(stem ${cpp} NAME_WE) set(test_target "boost_openmethod-ce-${stem}") diff --git a/ce/README.md b/ce/README.md index a98b2039..2c58f781 100644 --- a/ce/README.md +++ b/ce/README.md @@ -25,6 +25,20 @@ the policies, `inplace_vptr.hpp`. Two things to know: `-std=c++17 -O3 -DNDEBUG` is a good set of options to look at the generated code. +## Reflection + +`reflection.cpp` registers the classes by C++26 reflection (P2996). It includes +the same two headers as the other examples, but needs a compiler that +implements P2996 - *x86-64 gcc (trunk)* on Compiler Explorer - and +`-std=c++26 -freflection`. + +`Bulldog` is the point of the example. No overrider mentions it, and no +`BOOST_OPENMETHOD_CLASSES` lists it; the scan started by +`BOOST_OPENMETHOD_REGISTER_CLASSES()` finds it deriving from `Dog`, registers +it, and `poke` dispatches it to the overrider for `Dog`. + +## The sources + The sources in this directory are the examples published on Compiler Explorer. They are built and run as part of the test suite, and again against the flattened headers by `dev/check-flat.sh`, so a broken flattening is caught diff --git a/ce/reflection.cpp b/ce/reflection.cpp new file mode 100644 index 00000000..83962d64 --- /dev/null +++ b/ce/reflection.cpp @@ -0,0 +1,57 @@ +#include +#include +#include +#include + +struct Animal { + const char* name; + Animal(const char* name) : name(name) { + } + virtual ~Animal() { + } +}; + +struct Dog : Animal { + using Animal::Animal; +}; + +struct Cat : Animal { + using Animal::Animal; +}; + +// Named nowhere else: no overrider, no BOOST_OPENMETHOD_CLASSES. Only the scan +// finds it. +struct Bulldog : Dog { + using Dog::Dog; +}; + +using boost::openmethod::virtual_; + +BOOST_OPENMETHOD(poke, (virtual_, std::ostream&), void); + +BOOST_OPENMETHOD_OVERRIDE(poke, (Cat & animal, std::ostream& os), void) { + os << animal.name << " hisses.\n"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (Dog & animal, std::ostream& os), void) { + os << animal.name << " barks.\n"; +} + +BOOST_OPENMETHOD_REGISTER_CLASSES(); + +void poke_animals(const std::vector& animals, std::ostream& os) { + for (auto animal : animals) { + poke(*animal, os); + } +} + +auto main() -> int { + boost::openmethod::initialize(); + + Dog snoopy{"Snoopy"}; + Cat felix{"Felix"}; + Bulldog hector{"Hector"}; + std::vector animals = {&snoopy, &felix, &hector}; + + poke_animals(animals, std::cout); +} diff --git a/dev/check-flat.sh b/dev/check-flat.sh index 149f43c0..df848ad7 100755 --- a/dev/check-flat.sh +++ b/dev/check-flat.sh @@ -40,9 +40,50 @@ status=0 report() { printf '%-6s %s\n' "$1" "$2" - [ "$1" = ok ] || status=1 + [ "$1" = FAIL ] && status=1 + return 0 } +# ce/reflection.cpp needs C++26 reflection; everything else is C++17. Same +# probe as CMakeLists.txt: the standard alone is not enough, GCC wants a flag. +probe_reflection() { + cat > "$work/probe.cpp" <<'PROBE' +#include +struct Base {}; +struct Derived : Base {}; +consteval auto count() -> int { + return static_cast( + std::meta::bases_of( + ^^Derived, std::meta::access_context::unchecked()).size()); +} +static_assert(count() == 1); +int main() {} +PROBE + + for candidate in "${CXX_REFLECTION:-}" "$cxx" g++-16; do + [ -n "$candidate" ] || continue + command -v "$candidate" > /dev/null || continue + + for flags in "-std=c++26 -freflection" "-std=c++26"; do + # shellcheck disable=SC2086 + if "$candidate" $flags -fsyntax-only "$work/probe.cpp" \ + > /dev/null 2>&1; then + # shellcheck disable=SC2086 + reflection_compile=("$candidate" $flags) + return 0 + fi + done + done + + return 1 +} + +reflection_compile=() + +if probe_reflection; then + report note "C++26 reflection: ${reflection_compile[*]}" +fi + cd "$flat_dir" || exit 1 for header in $(find boost -name '*.hpp' | sort); do @@ -79,8 +120,20 @@ cd "$root_dir" || exit 1 for source in ce/*.cpp; do example="$work/$(basename "$source" .cpp)" + example_compile=("${compile[@]}") + + if [ "$source" = ce/reflection.cpp ]; then + if [ ${#reflection_compile[@]} -eq 0 ]; then + report skip "$source (no C++26 reflection)" + continue + fi + + example_compile=( + "${reflection_compile[@]}" "-I$flat_dir" "${boost_includes[@]}") + fi - if ! "${compile[@]}" -O2 -o "$example" "$source" > "$work/log" 2>&1; then + if ! "${example_compile[@]}" -O2 -o "$example" "$source" \ + > "$work/log" 2>&1; then report FAIL "$source" head -20 "$work/log" continue From b5f002e3f3fc538afbe48c514d452d95264e7954 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 4 Sep 2026 17:41:41 -0400 Subject: [PATCH 3/4] ce: link the reflection example on Compiler Explorer x86-64 gcc 16.2, -std=c++26 -freflection -O3 -DNDEBUG, Boost 1.90, and the two headers fetched from the Pages site. Checked through godbolt's own API: the compiler accepts the flags, and the example - with the flat headers pasted in place of the URL includes, as CE's client does - runs there and prints the expected three lines. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011HmyQexbZAWZk6BDkHSbdQ --- ce/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ce/README.md b/ce/README.md index 2c58f781..d8071638 100644 --- a/ce/README.md +++ b/ce/README.md @@ -37,6 +37,8 @@ implements P2996 - *x86-64 gcc (trunk)* on Compiler Explorer - and `BOOST_OPENMETHOD_REGISTER_CLASSES()` finds it deriving from `Dog`, registers it, and `poke` dispatches it to the overrider for `Dog`. +On Compiler Explorer: + ## The sources The sources in this directory are the examples published on Compiler Explorer. From 5ec487cb284faeac1ce4ebf478ca03491f959da5 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leroy Date: Fri, 4 Sep 2026 17:53:20 -0400 Subject: [PATCH 4/4] CI: deploy the flat headers from boostorg develop too The deploy step was gated on the fork, so merging this upstream would build the headers and then skip publishing them: boostorg.github.io/openmethod would keep 404ing on boost/openmethod.hpp. Upstream now deploys from develop - and only develop, since any other branch would clobber the site - while the fork keeps deploying from any branch, which is where a branch is tried out before it is merged. Upstream's Antora site starts being refreshed by this workflow again as a result; it has not been since 2026-07-15. The base URL is derived from github.repository_owner rather than hardcoded: it is baked into the banner and the guard-check message of every generated header, so it has to name the site the artifact is about to be deployed to. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011HmyQexbZAWZk6BDkHSbdQ --- .github/workflows/ci.yml | 16 +++++++++++++--- CLAUDE.md | 15 +++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48e1b966..df61ab65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,7 +191,12 @@ jobs: # Compiler Explorer example can include them by URL. See ce/README.md. - name: Build flat headers if: matrix.os == 'ubuntu-latest' - run: python3 dev/flatten.py --output-dir doc/html + # The base URL goes into the banner and the guard-check messages of + # every generated header, so it has to name the site this artifact is + # about to be deployed to - jll63's fork, or boostorg. + run: | + python3 dev/flatten.py --output-dir doc/html \ + --base-url "https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}" - name: Create Antora docs artifact uses: actions/upload-artifact@v4 @@ -205,9 +210,14 @@ jobs: with: path: doc/html - - name: Deploy to GitHub Pages (jll63) + # Upstream deploys from develop only - every other branch would clobber + # the site. The fork has no such restriction: it is where a branch is + # tried out before it is merged. + - name: Deploy to GitHub Pages if: >- matrix.os == 'ubuntu-latest' - && github.repository == 'jll63/openmethod' && github.event_name != 'pull_request' + && (github.repository == 'jll63/openmethod' + || (github.repository == 'boostorg/openmethod' + && github.ref_name == 'develop')) uses: actions/deploy-pages@v4 diff --git a/CLAUDE.md b/CLAUDE.md index 3bcfc180..82450ca3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -494,10 +494,17 @@ and the scan has to learn about it: miss one and the file still compiles, binds ### Flattened headers for Compiler Explorer `dev/flatten.py` rewrites every public header into a self-sufficient file under `flat/`; the -`antora` CI job regenerates them into the Pages artifact, so they are served from the root of -`https://jll63.github.io/openmethod` and a CE example can include them by URL. CE fetches those -includes client-side, which is why the host has to send CORS headers - GitHub Pages does, -`access-control-allow-origin: *`. +`antora` CI job regenerates them into the Pages artifact, so they are served from the root of the +site and a CE example can include them by URL. CE fetches those includes client-side, which is why +the host has to send CORS headers - GitHub Pages does, `access-control-allow-origin: *`. + +**Upstream deploys from `develop` only; the fork deploys from any branch.** Any other branch would +clobber the site, and the fork is where a branch is tried out before it is merged - which is why +the `Deploy to GitHub Pages` step tests the repository *and*, for boostorg, the ref. The two sites +are `https://boostorg.github.io/openmethod` and `https://jll63.github.io/openmethod`, and CI +derives `--base-url` from `github.repository_owner` rather than hardcoding either: that URL is +baked into every generated header's banner and guard-check messages, so it has to name the site +the artifact is about to be deployed to. The script's own default is the fork. The point of the exercise is that a CE example's include list matches a local one line for line: