Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6b02978
chore: adopt clang-format 22, tune two layout choices
jll63 Aug 29, 2026
a5f8f65
chore: put the colon at the end of the line
jll63 Aug 29, 2026
542bd5e
Merge branch 'chore/clang-format-22' into develop
jll63 Aug 29, 2026
d05496c
register classes using reflection if c++26
jll63 Aug 24, 2026
35c49e4
reflection: hold the scanned ranges in named locals
jll63 Aug 29, 2026
e2d53f4
ci: clone Boost.Test and Boost.DLL for the reflection job
jll63 Aug 29, 2026
9d08e46
reflection: name the exact gcc commit in the workaround comment
jll63 Aug 29, 2026
427f481
reflection: point the workaround comment at the upstream PRs
jll63 Aug 29, 2026
77996d8
reflection: generalize registration, rename it register_classes
jll63 Aug 29, 2026
a3e9807
doc: give the reflection API a reference, on MrDocs stubs
jll63 Aug 30, 2026
7294191
reflection: group the register_classes arguments, drop the scan options
jll63 Aug 31, 2026
68f281f
reflection: correct what the scan collects
jll63 Aug 31, 2026
47703e2
build: probe for -freflection instead of assuming every gcc takes it
jll63 Aug 31, 2026
3e694b3
test: fix the registrations the C++26 conversion got wrong
jll63 Aug 31, 2026
b095aa9
doc: let explicit_registration.hpp own the whole registry-override re…
jll63 Aug 31, 2026
79dcdca
test: find a method through a registrar alone, with no alias
jll63 Aug 31, 2026
ffa4228
reflection: fix register_classes<> with no groups at all
jll63 Aug 31, 2026
0eb5b7c
ci: name the scan by the name it now has
jll63 Sep 1, 2026
92e720c
build: refer to the reflection probe by project id
jll63 Sep 2, 2026
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
6 changes: 3 additions & 3 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ BraceWrapping:
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakInheritanceList: AfterColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakConstructorInitializers: AfterColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 80
Expand Down Expand Up @@ -102,7 +102,7 @@ PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Left
ReflowComments: false
SortIncludes: false
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,49 @@ jobs:
COVERITY_SCAN_NOTIFICATION_EMAIL: ${{ secrets.COVERITY_SCAN_NOTIFICATION_EMAIL }}
COVERITY_SCAN_TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }}

reflection:
name: C++26 reflection
runs-on: ubuntu-24.04
steps:
- 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 cmake ninja-build

- name: Clone Boost.OpenMethod
uses: actions/checkout@v4

- name: Clone Boost
uses: alandefreitas/cpp-actions/boost-clone@v1.8.8
with:
# boost-clone excludes `test` and `tests` from the scan
# (modules-exclude-paths defaults to them), so it never sees the
# includes in our own test/ and would clone neither Boost.Test nor
# Boost.DLL - CMake then fails to generate on a missing
# Boost::unit_test_framework. `modules` is unioned with the scan,
# and their own dependencies are resolved afterwards.
modules: test dll
branch: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }}
boost-dir: ../boost-source
scan-modules-dir: .
scan-modules-ignore: openmethod

# The suite is the reflection test: BOOST_OPENMETHOD_TEST_CLASSES expands
# to nothing here, so every class has to be found by the scan that
# BOOST_OPENMETHOD_REGISTER_CLASSES starts.
- name: Build and test
run: |
cmake -S . -B ../build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_COMPILER=g++-16 \
-DBOOST_OPENMETHOD_ENABLE_REFLECTION=ON \
-DBOOST_OPENMETHOD_BUILD_TESTS=ON \
-DBOOST_OPENMETHOD_WARNINGS_AS_ERRORS=ON \
-DBOOST_SRC_DIR="$(cd .. && pwd)/boost-source"
cmake --build ../build --target tests -j $(nproc)
ctest --test-dir ../build -j $(nproc) --output-on-failure

antora:
name: Antora docs
strategy:
Expand Down
54 changes: 51 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,61 @@ Two rules keep the reference clean; the long-form version lives next to the macr
Between them these cover every constraint in the library, so **do not declare a member twice**, an
unqualified copy under `#ifdef __MRDOCS__` beside the real one. Only MrDocs ever compiles that
copy, so the two drift apart silently and the reference then documents a constraint the library
does not have. The `#ifdef __MRDOCS__` blocks that remain remove declarations from the reference
(friends, deleted overloads, the `VirtualTraits` blueprint) rather than restate them.
does not have. Most of the `#ifdef __MRDOCS__` blocks therefore *remove* declarations from the
reference (friends, deleted overloads) or describe something that has no real counterpart at all
(the `VirtualTraits` blueprint).

Only expressions are affected: types are printed from the AST, so the macro may appear anywhere in
one (`method::operator()` takes
`typename BOOST_OPENMETHOD_UNLESS_MRDOCS(detail::) StripVirtualDecorator<Parameters>::type...` and
renders correctly). After a doc build, `grep -rl MRDOCS doc/html/` must return nothing.
renders correctly).

**The C++26 reflection API is the one exception, and it is a forced one.** MrDocs' front-end does
not implement P2996, so the reference build runs at `-D CMAKE_CXX_STANDARD=20` and
`BOOST_OPENMETHOD_HAS_REFLECTION` is 0 there: every `#if BOOST_OPENMETHOD_HAS_REFLECTION` block is
invisible to it. `register_classes` and `current_namespace` are therefore restated as
documentation stubs in an `#elif defined(__MRDOCS__)` branch at the end of `core.hpp`, and
`detail/reflection.hpp` forward-declares `std::meta::info` and `std::meta::access_context` under
`#ifdef __MRDOCS__` so those stubs can spell their real signatures. Two rules contain the drift:

- **Each doc comment exists exactly once, on the stub.** The real declaration carries only
`//! @see @ref <name> for documentation.`, as `inplace_vptr_derived` already does. Never copy
a doc comment into both branches.
- **Prefer widening a guard to writing a stub** whenever the code parses as C++20: a declaration
that is plain C++17 or C++20 can be guarded with
`#if BOOST_OPENMETHOD_HAS_REFLECTION || defined(__MRDOCS__)`, and MrDocs then reads the real
definition instead of a copy that can drift.

The `register_classes` stub is spelled `template<auto... Groups>` where the real one is
`template<detail::reflection_group... Groups>`. The stub drops the group type, which is an
implementation detail the reference has no reason to name - its braces are the only thing a
caller writes.

**A macro defined in both branches of an `#if` must carry its doc comment on the `#else` one.**
MrDocs compiles that branch, and a comment separated from its `#define` by preprocessor directives
is not attached to it - `BOOST_OPENMETHOD_REGISTER_CLASSES` silently produced no page at all until
its comment was moved down. Symptom to watch for: a `xref:reference:<NAME>.adoc` that renders as a
literal `href="#reference:<NAME>.adoc"`.

### Doc-comment markup traps

MrDocs parses `//!` comments as Markdown plus Doxygen commands, then emits AsciiDoc. Three shapes
mis-render silently; all three were found by rendering, none by reading the source:

- **A line starting with `- ` becomes a list item.** House style uses ` - ` as an em-dash, which is
fine mid-line but starts a stray bullet at the head of one. Rewrap so the dash never begins a
line, or reword to a semicolon.
- **`@ref` inside `**bold**` breaks the span**, leaving literal `**` in the output. Bold plain text
only: `**Options**: at most one @ref ... value`, not `**One @ref ... value**`.
- **An inline `` `^^::` `` loses both carets** and renders as `::`. Escape the first one -
`` `\^^::` `` - which comes through as `^^::`. Only this spelling is affected; `` `^^app` `` and
`^^::` inside an `@code` block are fine.

An `xref:reference:<name>.adoc` path works only for macros, which MrDocs puts at the top level.
A namespace-scoped symbol lives under `reference/boost/openmethod/`, so link it with
`cpp:<name>[]`, which resolves wherever the page ends up.

After a doc build, `grep -rl MRDOCS doc/html/` must return nothing.

## Common Development Patterns

Expand Down
75 changes: 75 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,81 @@ option(
BOOST_OPENMETHOD_WARNINGS_AS_ERRORS
"Treat warnings as errors"
OFF)
option(
BOOST_OPENMETHOD_ENABLE_REFLECTION
"Build the tests and examples with C++26 reflection enabled"
OFF)

# C++26 reflection (P2996). The library detects it on its own, from
# __cpp_impl_reflection; this only arranges for the tests to be built in a mode
# where the compiler provides it, which needs both C++26 and, on GCC, an opt-in
# flag. It is applied per target rather than through CMAKE_CXX_FLAGS, because
# CMake probes the compiler before CMAKE_CXX_STANDARD takes effect and GCC
# rejects -freflection under any other standard.
set(BOOST_OPENMETHOD_REFLECTION_OPTIONS "")

if (BOOST_OPENMETHOD_ENABLE_REFLECTION)
include(CheckCXXSourceCompiles)

set(BOOST_OPENMETHOD_REFLECTION_TEST_SOURCE [[
#include <meta>
struct Base {};
struct Derived : Base {};
consteval auto count() -> int {
return static_cast<int>(
std::meta::bases_of(
^^Derived, std::meta::access_context::unchecked()).size());
}
static_assert(count() == 1);
int main() {}
]])

set(CMAKE_REQUIRED_QUIET ON)

foreach(candidate "-std=c++26" "-std=c++26;-freflection")
string(REPLACE ";" " " candidate_flags "${candidate}")
set(CMAKE_REQUIRED_FLAGS "${candidate_flags}")
unset(BOOST_OPENMETHOD_HAS_REFLECTION CACHE)
check_cxx_source_compiles(
"${BOOST_OPENMETHOD_REFLECTION_TEST_SOURCE}"
BOOST_OPENMETHOD_HAS_REFLECTION)

if (BOOST_OPENMETHOD_HAS_REFLECTION)
set(BOOST_OPENMETHOD_REFLECTION_OPTIONS ${candidate})
break()
endif()
endforeach()

unset(CMAKE_REQUIRED_FLAGS)
unset(CMAKE_REQUIRED_QUIET)

if (NOT BOOST_OPENMETHOD_HAS_REFLECTION)
message(
FATAL_ERROR
"BOOST_OPENMETHOD_ENABLE_REFLECTION is ON but ${CMAKE_CXX_COMPILER_ID} "
"${CMAKE_CXX_COMPILER_VERSION} does not support C++26 reflection")
endif()

message(
STATUS
"Boost.OpenMethod: C++26 reflection enabled"
" [${BOOST_OPENMETHOD_REFLECTION_OPTIONS}]")
endif()

# Build `target` with C++26 reflection, if BOOST_OPENMETHOD_ENABLE_REFLECTION is
# ON. Does nothing otherwise, so callers need no condition of their own.
function(boost_openmethod_enable_reflection target)
if (NOT BOOST_OPENMETHOD_ENABLE_REFLECTION)
return()
endif()

# The standard flag is passed here rather than through CXX_STANDARD: CMake
# learned the value 26 only in 3.30, and this project supports older ones.
# target_compile_options come after the flag CMake derives from the
# library's cxx_std_17 requirement, and the last -std wins.
target_compile_options(
${target} PRIVATE ${BOOST_OPENMETHOD_REFLECTION_OPTIONS})
endfunction()

if (BOOST_OPENMETHOD_BUILD_EXAMPLES AND NOT BOOST_OPENMETHOD_BUILD_TESTS)
message(
Expand Down
19 changes: 19 additions & 0 deletions config/Jamfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Boost.OpenMethod Library Configuration Checks Jamfile
#
# 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

# The probe consulted by ../test/Jamfile. It sits here, and not under test/,
# because a b2 subproject inherits its parent's requirements: a check target
# declared in the test project would carry the very conditional it is being
# consulted for, and asking for it would ask for itself.
#
# The project has an id, and the test Jamfile refers to the probe by it: the
# requirement is inherited by the test subprojects too, and a relative
# reference would be resolved from each of their directories.

project /boost/openmethod/config ;

obj has_reflection : has_reflection.cpp : <cxxflags>-freflection ;
explicit has_reflection ;
20 changes: 20 additions & 0 deletions config/has_reflection.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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)

// Probe for C++26 reflection (P2996), compiled with -freflection. See
// ../Jamfile.

#include <meta>

struct Base {};
struct Derived : Base {};

consteval auto count() -> int {
return static_cast<int>(
std::meta::bases_of(^^Derived, std::meta::access_context::unchecked())
.size());
}

static_assert(count() == 1);
2 changes: 2 additions & 0 deletions doc/modules/ROOT/examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ foreach (cpp ${cpp_files})
get_filename_component(stem ${cpp} NAME_WE)
set(test_target "boost_openmethod-${stem}")
add_executable(${test_target} ${cpp})
boost_openmethod_enable_reflection(${test_target})
target_link_libraries(${test_target} PRIVATE Boost::openmethod Boost::unit_test_framework)
add_test(NAME ${test_target} COMMAND ${test_target})
add_dependencies(tests ${test_target})
Expand All @@ -43,6 +44,7 @@ function(boost_openmethod_add_step_by_step dir)
file(GLOB cpp_files "${subdir}/*.cpp")
set(target "boost_openmethod-${dir}_${subex}")
add_executable(${target} ${cpp_files})
boost_openmethod_enable_reflection(${target})
target_link_libraries(${target} PRIVATE Boost::openmethod)
set(output_dir openmethod/${dir}/${subex})
set_target_properties(${target} PROPERTIES
Expand Down
14 changes: 8 additions & 6 deletions doc/modules/ROOT/examples/accept_no_visitors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ struct Node {
struct Plus : Node {
Plus(
shared_virtual_ptr<const Node> left,
shared_virtual_ptr<const Node> right)
: left(std::move(left)), right(std::move(right)) {
shared_virtual_ptr<const Node> right) :
left(std::move(left)), right(std::move(right)) {
}

shared_virtual_ptr<const Node> left, right;
Expand All @@ -34,8 +34,8 @@ struct Plus : Node {
struct Times : Node {
Times(
shared_virtual_ptr<const Node> left,
shared_virtual_ptr<const Node> right)
: left(std::move(left)), right(std::move(right)) {
shared_virtual_ptr<const Node> right) :
left(std::move(left)), right(std::move(right)) {
}

shared_virtual_ptr<const Node> left, right;
Expand Down Expand Up @@ -82,7 +82,8 @@ BOOST_OPENMETHOD_OVERRIDE(as_forth, (virtual_ptr<const Times> node), string) {
return as_forth(node->left) + " " + as_forth(node->right) + " *";
}

BOOST_OPENMETHOD_OVERRIDE(as_forth, (virtual_ptr<const Variable> node), string) {
BOOST_OPENMETHOD_OVERRIDE(
as_forth, (virtual_ptr<const Variable> node), string) {
return std::to_string(node->value);
}

Expand Down Expand Up @@ -111,7 +112,8 @@ auto main() -> int {
shared_virtual_ptr<Node> node = make_shared_virtual<Times>(
make_shared_virtual<Variable>(2),
make_shared_virtual<Plus>(
make_shared_virtual<Variable>(3), make_shared_virtual<Variable>(4)));
make_shared_virtual<Variable>(3),
make_shared_virtual<Variable>(4)));

cout << as_forth(node) << " = " << as_lisp(node) << " = " << value(node)
<< "\n";
Expand Down
3 changes: 1 addition & 2 deletions doc/modules/ROOT/examples/inplace_vptr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ struct Cat : Animal, inplace_vptr_derived<Cat, Animal> {};

struct Dog : Animal, inplace_vptr_derived<Dog, Animal> {};

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

BOOST_OPENMETHOD_OVERRIDE(poke, (Cat&, std::ostream& os), void) {
os << "hiss\n";
Expand Down
4 changes: 2 additions & 2 deletions doc/modules/ROOT/examples/matrix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ struct abstract {
int ref_count = 0;
};

struct registry
: boost::openmethod::registry<boost::openmethod::policies::static_rtti> {};
struct registry :
boost::openmethod::registry<boost::openmethod::policies::static_rtti> {};

template<class Rep>
using matrix_ptr = boost::openmethod::virtual_ptr<Rep, registry>;
Expand Down
9 changes: 6 additions & 3 deletions doc/modules/ROOT/examples/matrix_readme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ BOOST_OPENMETHOD_CLASSES(Matrix, SquareMatrix, SymmetricMatrix, DiagonalMatrix);

BOOST_OPENMETHOD(to_json, (virtual_ptr<const Matrix>, std::ostream& os), void);

BOOST_OPENMETHOD_OVERRIDE(to_json, (virtual_ptr<const SquareMatrix>, std::ostream& os), void) {
BOOST_OPENMETHOD_OVERRIDE(
to_json, (virtual_ptr<const SquareMatrix>, std::ostream& os), void) {
os << "all the elements\n";
}

BOOST_OPENMETHOD_OVERRIDE(to_json, (virtual_ptr<const SymmetricMatrix>, std::ostream& os), void) {
BOOST_OPENMETHOD_OVERRIDE(
to_json, (virtual_ptr<const SymmetricMatrix>, std::ostream& os), void) {
os << "elements above and including the diagonal\n";
}

BOOST_OPENMETHOD_OVERRIDE(to_json, (virtual_ptr<const DiagonalMatrix>, std::ostream& os), void) {
BOOST_OPENMETHOD_OVERRIDE(
to_json, (virtual_ptr<const DiagonalMatrix>, std::ostream& os), void) {
os << "just the diagonal\n";
}

Expand Down
5 changes: 3 additions & 2 deletions doc/modules/ROOT/examples/rolex/1/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ int main() {
boost::openmethod::initialize();

Employee bill;
Salesman bob; bob.sales = 100'000.0;
Salesman bob;
bob.sales = 100'000.0;

std::cout << "pay bill: $" << pay(bill) << "\n"; // pay bill: $5000
std::cout << "pay bob: $" << pay(bob) << "\n"; // pay bob: $10000
std::cout << "pay bob: $" << pay(bob) << "\n"; // pay bob: $10000
}
// end::content[]
4 changes: 3 additions & 1 deletion doc/modules/ROOT/examples/rolex/1/roles.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

#include <boost/openmethod.hpp>

struct Employee { virtual ~Employee() = default; };
struct Employee {
virtual ~Employee() = default;
};

struct Salesman : Employee {
double sales = 0.0;
Expand Down
5 changes: 3 additions & 2 deletions doc/modules/ROOT/examples/rolex/2/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ int main() {
boost::openmethod::initialize();

Employee bill;
Salesman bob; bob.sales = 100'000.0;
Salesman bob;
bob.sales = 100'000.0;

std::cout << "pay bill: $" << pay(bill) << "\n"; // pay bill: $5000
std::cout << "pay bob: $" << pay(bob) << "\n"; // pay bob: $10000
std::cout << "pay bob: $" << pay(bob) << "\n"; // pay bob: $10000
}
// end::content[]
Loading