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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,16 @@ jobs:

# 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.
# BOOST_OPENMETHOD_REGISTER_CLASSES starts. BUILD_SHARED_LIBS brings in
# test/dynamic_loading, where the registrar that scan instantiates - one
# inline variable per class, for the whole program - crosses module
# boundaries.
- name: Build and test
run: |
cmake -S . -B ../build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_COMPILER=g++-16 \
-DBUILD_SHARED_LIBS=ON \
-DBOOST_OPENMETHOD_ENABLE_REFLECTION=ON \
-DBOOST_OPENMETHOD_BUILD_TESTS=ON \
-DBOOST_OPENMETHOD_WARNINGS_AS_ERRORS=ON \
Expand Down
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ cmake --build .
- `BOOST_OPENMETHOD_BUILD_TESTS` - Enable tests (default: ON if root project)
- `BOOST_OPENMETHOD_BUILD_EXAMPLES` - Enable examples (requires tests enabled)
- `BOOST_OPENMETHOD_WARNINGS_AS_ERRORS` - Treat warnings as errors
- `BOOST_OPENMETHOD_ENABLE_REFLECTION` - Build the tests and examples with C++26 reflection; probes the compiler for the flags it needs (`-std=c++26 -freflection` on GCC 16) and fails the configure if it has none
- `BOOST_SRC_DIR` - Path to Boost source directory (default: `../..` or `$BOOST_SRC_DIR` env var)

### Boost.Build (b2)
Expand Down Expand Up @@ -356,8 +357,8 @@ 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:
MrDocs parses `//!` comments as Markdown plus Doxygen commands, then emits AsciiDoc. Four shapes
mis-render silently; all four 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
Expand All @@ -367,6 +368,9 @@ mis-render silently; all three were found by rendering, none by reading the sour
- **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.
- **`@attention` is dropped silently**, paragraph and all: no admonition, no text, no warning.
MrDocs knows `@note` and `@warning`, which render as NOTE and WARNING blocks wherever they sit in
the description - the first paragraph after the brief included. Use one of those.

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
Expand Down
85 changes: 44 additions & 41 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,47 +65,55 @@ option(
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.
# __cpp_impl_reflection; this only arranges for the tests and examples to be
# built in a mode where the compiler provides it, which needs both C++26 and, on
# GCC, an opt-in flag. The options are added to this directory - every test,
# example and snippet, the shared-library ones in the subdirectories included -
# rather than to 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)
# The probe passes its own -std flag, and try_compile appends the one it
# derives from CMAKE_CXX_STANDARD *after* the definitions (policy CMP0067),
# where it would win: a configure that sets the standard - a preset, a
# toolchain file, a consuming project - would fail both candidates and stop
# at the FATAL_ERROR below. Take the variables out of the probe's way, and
# put them back after. Set to nothing rather than unset: a normal variable
# hides a cache one - a -D on the command line - where unset would expose
# it, and try_compile leaves an empty standard alone. The probe source is
# the one b2 uses (config/Jamfile); like b2, compile it only - it has no
# main - which the target type arranges.
foreach(var CMAKE_CXX_STANDARD CMAKE_CXX_EXTENSIONS CMAKE_TRY_COMPILE_TARGET_TYPE)
set(BOOST_OPENMETHOD_SAVED_${var} "${${var}}")
set(${var} "")
endforeach()
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

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)
try_compile(
BOOST_OPENMETHOD_HAS_REFLECTION
"${CMAKE_BINARY_DIR}/CMakeFiles/CMakeTmp"
SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/config/has_reflection.cpp"
COMPILE_DEFINITIONS ${candidate})

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

unset(CMAKE_REQUIRED_FLAGS)
unset(CMAKE_REQUIRED_QUIET)
# An empty one is unset, not set to nothing, which a target would take for
# an invalid standard.
foreach(var CMAKE_CXX_STANDARD CMAKE_CXX_EXTENSIONS CMAKE_TRY_COMPILE_TARGET_TYPE)
if ("${BOOST_OPENMETHOD_SAVED_${var}}" STREQUAL "")
unset(${var})
else()
set(${var} "${BOOST_OPENMETHOD_SAVED_${var}}")
endif()
endforeach()

if (NOT BOOST_OPENMETHOD_HAS_REFLECTION)
message(
Expand All @@ -118,22 +126,17 @@ if (BOOST_OPENMETHOD_ENABLE_REFLECTION)
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()
# Directory compile options come after the flag CMake derives from the
# library's cxx_std_17 requirement, and the last -std wins. The definition
# makes a target that does not end up with reflection fail to build, instead
# of quietly falling back to explicit class registration: see
# test/test_classes.hpp.
add_compile_options(${BOOST_OPENMETHOD_REFLECTION_OPTIONS})
add_compile_definitions(BOOST_OPENMETHOD_EXPECT_REFLECTION)
endif()

if (BOOST_OPENMETHOD_BUILD_EXAMPLES AND NOT BOOST_OPENMETHOD_BUILD_TESTS)
message(
Expand Down
1 change: 0 additions & 1 deletion ce/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ 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})
Expand Down
2 changes: 0 additions & 2 deletions doc/modules/ROOT/examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ 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 @@ -44,7 +43,6 @@ 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
54 changes: 32 additions & 22 deletions doc/modules/ROOT/pages/basics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -175,19 +175,33 @@ 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.

The namespaces decide where the *derived* classes are looked for, not which
bases are recorded. A class between a registered class and a root is registered
too, wherever it is declared - in a namespace the scan does not enter, or as a
specialization of a class template, which the scan cannot find on its own - so
that the inheritance lattice is complete. And each class is recorded with every
registered class above it, so that a registration stands on its own, whatever
another one - in another translation unit, over other namespaces - registers
alongside it.

A scan does not descend into the `std` and `boost` namespaces, so scanning the
global namespace costs little more than a narrower one would - a method cannot
dispatch on a class the program never heard of anyway. The exclusion applies to
recursion only: a namespace *listed* explicitly is always scanned, which is how
a class in `std` or `boost` gets registered.

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.
The scan runs when the registrar the macro declares is instantiated, which
current compilers do at the end of the translation unit: it sees the whole
file, wherever the macro sits. The standard promises less. A class or a method
the scan would select, declared after the macro in the same translation unit,
makes the program ill-formed, no diagnostic required. So put the macro *after*
the declarations it is meant to find: at the bottom of the file.

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.
error here: a class is recorded under the bases it reaches unambiguously
through public inheritance, and a base it inherits more than once - which no
reference to it can be converted to - is left out of its list.

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
Expand All @@ -199,26 +213,22 @@ 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;
always found. A method's return type is not a root: a covariant return type is
registered like any other class, when it derives from a root, or when it is
listed.

Four situations remain outside the scan's reach, and need to be registered by
other means - listed in the macro, or given a `BOOST_OPENMETHOD_CLASSES` of
their own:

* a class in a namespace the macro does not scan, unless it sits between a
class the scan finds and a root;
* a specialization of a class template that no method dispatches on, and that
no class the scan finds derives from - one used only as an overrider
parameter, say. Specializations are not members of a namespace, so the scan
does not see them; list it: `BOOST_OPENMETHOD_REGISTER_CLASSES({^^Pet<int>})`;
* 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> {};
----
4 changes: 2 additions & 2 deletions doc/modules/ROOT/pages/core_api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ BOOST_OPENMETHOD_REGISTER(register_classes<>);

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
The registration must come after the declarations it is meant to find, at the
bottom of the file. See
xref:ROOT:basics.adoc#registering_classes_by_reflection[Registering Classes by
Reflection].

Expand Down
9 changes: 0 additions & 9 deletions doc/modules/ROOT/pages/registries_and_policies.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,6 @@ 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
cpp: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: 0 additions & 1 deletion doc/modules/ROOT/snippets/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ 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: 0 additions & 2 deletions doc/modules/ROOT/snippets/errors_missing_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
// 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: 0 additions & 2 deletions doc/modules/ROOT/snippets/errors_missing_class_call.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
// 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: 0 additions & 2 deletions doc/modules/ROOT/snippets/errors_missing_class_method.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
// 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,8 +3,6 @@
// 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
33 changes: 0 additions & 33 deletions doc/modules/ROOT/snippets/explicit_registration.hpp

This file was deleted.

Loading
Loading