Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,37 @@ 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:
# 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

- 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:
Expand Down Expand Up @@ -156,6 +187,17 @@ 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'
# 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
with:
Expand All @@ -168,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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
47 changes: 47 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,53 @@ 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 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:

```cpp
#include <https://jll63.github.io/openmethod/boost/openmethod.hpp>
#include <https://jll63.github.io/openmethod/boost/openmethod/initialize.hpp>
```

`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 "<boost/openmethod/initialize.hpp>: #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 <boost/openmethod/...>` 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 `<typeinfo>` 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/`.

Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
2 changes: 2 additions & 0 deletions ce/2-method.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ struct Cat : Animal {

BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);

using boost::openmethod::virtual_;

BOOST_OPENMETHOD(
meet, (virtual_<Animal&>, virtual_<Animal&>, std::ostream&), void);

Expand Down
49 changes: 23 additions & 26 deletions ce/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
# 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")

# 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}")
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()
66 changes: 47 additions & 19 deletions ce/README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,54 @@
# 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 <https://jll63.github.io/openmethod>, 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 <https://jll63.github.io/openmethod/boost/openmethod.hpp>
#include <https://jll63.github.io/openmethod/boost/openmethod/initialize.hpp>
```

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).
## 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`.

On Compiler Explorer: <https://godbolt.org/z/1eTx5f13z>

## 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
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
```
57 changes: 57 additions & 0 deletions ce/reflection.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#include <iostream>
#include <vector>
#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>

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_<Animal&>, 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<Animal*>& 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<Animal*> animals = {&snoopy, &felix, &hector};

poke_animals(animals, std::cout);
}
2 changes: 2 additions & 0 deletions ce/uni-method.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ struct Cat : Animal {

BOOST_OPENMETHOD_CLASSES(Animal, Dog, Cat);

using boost::openmethod::virtual_;

BOOST_OPENMETHOD(poke, (virtual_<Animal&>, std::ostream&), void);

BOOST_OPENMETHOD_OVERRIDE(poke, (Cat & animal, std::ostream& os), void) {
Expand Down
Loading
Loading