Skip to content
Open
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
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,41 @@ 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:
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 use_classes_in.
- 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
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
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
92 changes: 92 additions & 0 deletions doc/modules/ROOT/pages/basics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ direct base of a class must appear together with it in at least one call to
`BOOST_OPENMETHOD_CLASSES`. This enables the library to deduce the complete
inheritance lattice.

[NOTE]
====
With a compiler that supports C++26 reflection, the class list is unnecessary.
xref:reference:BOOST_OPENMETHOD_CLASSES_IN.adoc[BOOST_OPENMETHOD_CLASSES_IN]
finds the classes on its own - see <<registering_classes_by_reflection>>.
====

The constructs used in this example require the classes to be polymorphic, in
the standard C++ sense, i.e. they must have at least one virtual function. The
library can also be used with non-polymorphic classes, with some restrictions.
Expand All @@ -107,3 +114,88 @@ Putting it all together:
----
include::{examplesdir}/ast.cpp[tag=content]
----


[#registering_classes_by_reflection]
## Registering Classes by Reflection

When the compiler supports C++26 reflection (P2996), the library can work out
the class list for itself. One call to
xref:reference:BOOST_OPENMETHOD_CLASSES_IN.adoc[BOOST_OPENMETHOD_CLASSES_IN]
replaces every
xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[BOOST_OPENMETHOD_CLASSES] in the
file:

[source,c++]
----
struct Animal { virtual ~Animal() = default; };
struct Cat : Animal {};
struct Dog : Animal {};
struct Bulldog : Dog {};

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

BOOST_OPENMETHOD_OVERRIDE(poke, (std::ostream& os, Dog&), void) {
os << "bark";
}

BOOST_OPENMETHOD_CLASSES_IN(::); // registers all four classes
----

The macro scans the namespace it is given - and the namespaces nested in it -
for the methods of the registry. It collects the classes those methods dispatch
on, then registers them, along with every class in the scanned namespaces that
derives from one of them. `Bulldog` above has no overrider of its own and is
named nowhere, and is registered all the same.

A base class that no method dispatches on is *not* registered - it could never
be selected on, and registering it would cost a lattice node, a hash slot and
dispatch table space for nothing. So a hierarchy rooted in some general-purpose
base contributes only the part of itself that takes part in dispatch. As soon as
another method does dispatch on that base, it is registered, and the inheritance
edges through it with it.

Classes declared in the standard library's or the compiler's own headers are not
scanned, so `::` costs little more than the narrower namespace would.

Reflection sees only what precedes it, so the macro must come *after* the
declarations it is meant to find. Putting it at the bottom of the file is the
simplest way to be sure.

Virtual and multiple inheritance are supported. Unlike
`BOOST_OPENMETHOD_CLASSES`, which rejects it, repeated inheritance is not an
error here: an ambiguous base cannot take part in dispatch, so it is left out.

Without reflection - in C++17, or in C++26 without the compiler flag that enables
it - the macro expands to nothing. A file that also calls
`BOOST_OPENMETHOD_CLASSES` therefore builds under either standard.

### What Reflection Cannot Find

A method is found through any declaration that names its `method` type: the
alias `BOOST_OPENMETHOD` declares alongside the method, a `using` declaration of
your own, or any of the method's registrar objects. None of those depends on the
method having an overrider, so a method declared with `BOOST_OPENMETHOD` is
always found.

Three situations remain outside the scan's reach, and need a
`BOOST_OPENMETHOD_CLASSES` of their own:

* a class in a namespace the macro does not scan;
* a core API method whose `method<...>` type is spelled out in full at every
use, with no `using` declaration of its own and no overrider - nothing names
it;
* a program that declares no method at all, and uses `virtual_ptr` on its own -
there is no virtual parameter for the scan to start from.

### Turning It Off

Adding the `policies::explicit_class_registration` policy to a registry stops
the library from registering anything on its own, in C++26 as in C++17:

[source,c++]
----
struct my_registry
: boost::openmethod::default_registry::with<
boost::openmethod::policies::explicit_class_registration> {};
----
16 changes: 16 additions & 0 deletions doc/modules/ROOT/pages/core_api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ We register the classes with `use_classes`:
include::{example}/core_api.cpp[tag=use_classes]
----

With a compiler that supports C++26 reflection, `use_classes_in` replaces that
list. It scans a namespace for the registry's methods, and registers the classes
they dispatch on along with everything in the namespace that derives from them:

[source,c++]
----
BOOST_OPENMETHOD_REGISTER(use_classes_in<^^::>);
----

A method declared the way `postfix` is above - an alias for a `method`
specialization - is found directly, and so is any of its `override` registrars.
Reflection sees only what precedes it, so this must come after the declarations
it is meant to find. See
xref:ROOT:basics.adoc#registering_classes_by_reflection[Registering Classes by
Reflection].

Finally, we call the method via the static member of the method class `fn`:

[source,c++]
Expand Down
2 changes: 2 additions & 0 deletions doc/modules/ROOT/pages/ref_macros.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ uses of the library.
| Adds an overrider to a method.
| xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[*BOOST_OPENMETHOD_CLASSES*]
| Registers classes.
| xref:reference:BOOST_OPENMETHOD_CLASSES_IN.adoc[*BOOST_OPENMETHOD_CLASSES_IN*]
| Registers the classes of a namespace, by reflection.
| xref:reference:BOOST_OPENMETHOD_INLINE_OVERRIDE.adoc[BOOST_OPENMETHOD_INLINE_OVERRIDE]
| Adds an overrider to a method as an inline function.
| xref:reference:BOOST_OPENMETHOD_DECLARE_OVERRIDER.adoc[BOOST_OPENMETHOD_DECLARE_OVERRIDER]
Expand Down
9 changes: 9 additions & 0 deletions doc/modules/ROOT/pages/registries_and_policies.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ is defined, `default_registry` also contains the `runtime_checks` policy. This
enables extra validations during method dispatch, which can detect missing class
registrations that could not be caught by `initialize`.

The `explicit_class_registration` policy does the opposite of adding a
behaviour: it stops the library from registering classes by reflection, so a
registry that contains it knows only the classes named in
xref:reference:BOOST_OPENMETHOD_CLASSES.adoc[BOOST_OPENMETHOD_CLASSES] or
xref:reference:use_classes.adoc[use_classes]. It has no effect if the compiler
does not support C++26 reflection. See
xref:ROOT:basics.adoc#registering_classes_by_reflection[Registering Classes by
Reflection].

The library provides another predefined registry: cpp:indirect_registry[]. It is
useful when shared libraries are dynamically loaded at runtime, and add methods
and overriders across program and shared library boundaries. See the section
Expand Down
1 change: 1 addition & 0 deletions doc/modules/ROOT/snippets/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ foreach (cpp ${cpp_files})
get_filename_component(stem ${cpp} NAME_WE)
set(test_target "boost_openmethod-snippet_${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 Down
2 changes: 2 additions & 0 deletions doc/modules/ROOT/snippets/errors_missing_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// See accompanying file LICENSE_1_0.txt
// or copy at http://www.boost.org/LICENSE_1_0.txt)

#include "explicit_registration.hpp"

#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>

Expand Down
2 changes: 2 additions & 0 deletions doc/modules/ROOT/snippets/errors_missing_class_call.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// which `default_registry` carries only when this symbol is defined.
#define BOOST_OPENMETHOD_ENABLE_RUNTIME_CHECKS

#include "explicit_registration.hpp"

#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>

Expand Down
2 changes: 2 additions & 0 deletions doc/modules/ROOT/snippets/errors_missing_class_method.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// See accompanying file LICENSE_1_0.txt
// or copy at http://www.boost.org/LICENSE_1_0.txt)

#include "explicit_registration.hpp"

#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// See accompanying file LICENSE_1_0.txt
// or copy at http://www.boost.org/LICENSE_1_0.txt)

#include "explicit_registration.hpp"

#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>

Expand Down
27 changes: 27 additions & 0 deletions doc/modules/ROOT/snippets/explicit_registration.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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)

// Makes the default registry require explicit class registration, so that the
// error snippets keep reporting the error they illustrate when the compiler
// supports C++26 reflection. Include *before* <boost/openmethod.hpp>. Like
// error_harness.hpp, never part of a tagged region: the pages show the mistake
// and the operation that reports it, and nothing else.
//
// The errors themselves do not go away in C++26 -- a class the library cannot
// reach from a method signature, an overrider, or a virtual_ptr still has to be
// registered by hand -- but these particular examples are all within its reach.

#ifndef BOOST_OPENMETHOD_SNIPPETS_EXPLICIT_REGISTRATION_HPP
#define BOOST_OPENMETHOD_SNIPPETS_EXPLICIT_REGISTRATION_HPP

#include <boost/openmethod/default_registry.hpp>

struct snippet_registry
: boost::openmethod::default_registry::with<
boost::openmethod::policies::explicit_class_registration> {};

#define BOOST_OPENMETHOD_DEFAULT_REGISTRY snippet_registry

#endif
Loading
Loading