From 5274f2cf68a25e6d2e17912c1c62ede68d13d094 Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Fri, 9 Oct 2026 12:27:18 -0600 Subject: [PATCH] fix(any_executor): support builds without RTTI any_executor uses detail::type_id to identify the wrapped executor type. It does not use typeid. The header compiles with RTTI disabled. Remove target_type(). Its return type would have been detail::type_info const&, so code that builds both with and without RTTI could only compare it against a detail type. This is a breaking change. Use target() instead. Add target() in const and non-const forms, as in executor_ref. It returns a pointer to the wrapped executor. It returns nullptr if the type does not match or if the any_executor is empty. executor_ref::target() is now noexcept. It returns nullptr when the executor_ref is empty instead of dereferencing a null vtable. Add a test that compiles and runs with RTTI disabled. Closes #448 --- include/boost/capy/ex/any_executor.hpp | 61 +++++++++--- include/boost/capy/ex/executor_ref.hpp | 19 ++-- test/CMakeLists.txt | 2 + test/Jamfile | 2 + test/no_rtti/CMakeLists.txt | 26 ++++++ test/no_rtti/Jamfile | 26 ++++++ test/no_rtti/any_executor.cpp | 123 +++++++++++++++++++++++++ test/unit/ex/any_executor.cpp | 81 ++++++++++++++-- test/unit/ex/executor_ref.cpp | 15 +++ 9 files changed, 325 insertions(+), 30 deletions(-) create mode 100644 test/no_rtti/CMakeLists.txt create mode 100644 test/no_rtti/Jamfile create mode 100644 test/no_rtti/any_executor.cpp diff --git a/include/boost/capy/ex/any_executor.hpp b/include/boost/capy/ex/any_executor.hpp index 28b2a8c36..0e0fff33b 100644 --- a/include/boost/capy/ex/any_executor.hpp +++ b/include/boost/capy/ex/any_executor.hpp @@ -13,11 +13,11 @@ #include #include +#include #include #include #include #include -#include namespace boost { namespace capy { @@ -54,10 +54,10 @@ struct is_strand_type> : std::true_type {}; @par Default State A default-constructed `any_executor` holds no executor. - `operator bool()`, `operator==`, and `target_type()` report the - empty state. `context()`, `on_work_started()`, `on_work_finished()`, - `dispatch()`, and `post()` are undefined behavior until an - executor is assigned. + `operator bool()`, `operator==`, and `target()` report the + empty state. `context()`, `on_work_started()`, + `on_work_finished()`, `dispatch()`, and `post()` are undefined + behavior until an executor is assigned. @par Thread Safety @@ -92,7 +92,8 @@ class any_executor virtual std::coroutine_handle<> dispatch(continuation&) const = 0; virtual void post(continuation&) const = 0; virtual bool equals(impl_base const*) const noexcept = 0; - virtual std::type_info const& target_type() const noexcept = 0; + virtual detail::type_info const& target_type() const noexcept = 0; + virtual void const* target_ptr() const noexcept = 0; }; template @@ -138,9 +139,14 @@ class any_executor return ex_ == static_cast(other)->ex_; } - std::type_info const& target_type() const noexcept override + detail::type_info const& target_type() const noexcept override { - return typeid(Ex); + return detail::type_id(); + } + + void const* target_ptr() const noexcept override + { + return &ex_; } }; @@ -300,16 +306,41 @@ class any_executor return p_->equals(other.p_.get()); } - /** Returns the type_info of the wrapped executor. + /** Return a pointer to the wrapped executor if it matches + the requested type. + + Analogous to `std::any_cast(&a)`. Top-level cv-qualifiers + on `Ex` are ignored when matching. + + Copies of an `any_executor` share one stored executor, so + every copy returns the same address. A modification made + through the non-const overload is visible to all of them. + Such modifications are not synchronized with concurrent use + of any copy. + + @tparam Ex The executor type to retrieve. - @return The `std::type_info` of the stored executor type, - or `typeid(void)` if empty. + @return A pointer to the stored executor, or `nullptr` if + this instance is empty or holds a different type. */ - std::type_info const& target_type() const noexcept + template + Ex const* target() const noexcept + { + if(!p_ || p_->target_type() != detail::type_id()) + return nullptr; + // Casting from void const* avoids naming impl, which would + // instantiate its overrides and reject non-executor types + return static_cast(p_->target_ptr()); + } + + /// @copydoc target() const + template + Ex* target() noexcept { - if(!p_) - return typeid(void); - return p_->target_type(); + if(!p_ || p_->target_type() != detail::type_id()) + return nullptr; + return const_cast( + static_cast(p_->target_ptr())); } }; diff --git a/include/boost/capy/ex/executor_ref.hpp b/include/boost/capy/ex/executor_ref.hpp index 7ba625e4e..6d91bc099 100644 --- a/include/boost/capy/ex/executor_ref.hpp +++ b/include/boost/capy/ex/executor_ref.hpp @@ -108,10 +108,10 @@ class executor_ref public: /** Construct a default instance. - Constructs an empty `executor_ref`. `operator bool()` and - `operator==()` report the empty state; `context()`, - `on_work_started()`, `on_work_finished()`, `dispatch()`, - `post()`, and `target()` are undefined behavior until an + Constructs an empty `executor_ref`. `operator bool()`, + `operator==()`, and `target()` report the empty state; + `context()`, `on_work_started()`, `on_work_finished()`, + `dispatch()`, and `post()` are undefined behavior until an executor is assigned. */ executor_ref() = default; @@ -265,21 +265,22 @@ class executor_ref @tparam Executor The executor type to retrieve. @return A pointer to the underlying executor, or - `nullptr` if the type does not match. + `nullptr` if this reference is empty or the type + does not match. */ template< typename Executor > - const Executor* target() const + const Executor* target() const noexcept { - if ( *vt_->type_id == detail::type_id< Executor >() ) + if ( vt_ && *vt_->type_id == detail::type_id< Executor >() ) return static_cast< Executor const* >( ex_ ); return nullptr; } /// @copydoc target() const template< typename Executor> - Executor* target() + Executor* target() noexcept { - if ( *vt_->type_id == detail::type_id< Executor >() ) + if ( vt_ && *vt_->type_id == detail::type_id< Executor >() ) return const_cast< Executor* >( static_cast< Executor const* >( ex_ )); return nullptr; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 858554619..7483cd92c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,5 +1,6 @@ # # Copyright (c) 2025 Mohammad Nejati +# Copyright (c) 2026 Michael Vandeberg # # 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) @@ -13,4 +14,5 @@ if(NOT TARGET tests) endif() add_subdirectory(unit) +add_subdirectory(no_rtti) add_subdirectory(doc) diff --git a/test/Jamfile b/test/Jamfile index 722a51ddc..78c1b93c5 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -1,5 +1,6 @@ # # Copyright (c) 2025 Mohammad Nejati +# Copyright (c) 2026 Michael Vandeberg # # 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) @@ -8,4 +9,5 @@ # build-project unit ; +build-project no_rtti ; build-project doc ; diff --git a/test/no_rtti/CMakeLists.txt b/test/no_rtti/CMakeLists.txt new file mode 100644 index 000000000..8d1b45708 --- /dev/null +++ b/test/no_rtti/CMakeLists.txt @@ -0,0 +1,26 @@ +# +# Copyright (c) 2026 Michael Vandeberg +# +# 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) +# +# Official repository: https://github.com/cppalliance/capy +# + +# Built with RTTI disabled. The capy library it links is not, which is +# why any_executor.cpp may only type-erase executor types it defines. +add_executable(boost_capy_tests_no_rtti any_executor.cpp) +target_link_libraries( + boost_capy_tests_no_rtti PRIVATE + Boost::capy_test_suite_main + Boost::capy) + +if(MSVC) + target_compile_options(boost_capy_tests_no_rtti PRIVATE /GR-) +else() + target_compile_options(boost_capy_tests_no_rtti PRIVATE -fno-rtti) +endif() + +boost_capy_test_suite_discover_tests(boost_capy_tests_no_rtti) + +add_dependencies(tests boost_capy_tests_no_rtti) diff --git a/test/no_rtti/Jamfile b/test/no_rtti/Jamfile new file mode 100644 index 000000000..3f935961f --- /dev/null +++ b/test/no_rtti/Jamfile @@ -0,0 +1,26 @@ +# +# Copyright (c) 2026 Michael Vandeberg +# +# 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) +# +# Official repository: https://github.com/cppalliance/capy +# + +import ac ; +import testing ; + +project + : requirements + $(c20-requires) + /boost/capy//boost_capy + ../../extra/test_suite/test_main.cpp + ../../extra/test_suite/test_suite.cpp + ../../extra/test_suite + off + extra + on + darwin,norecover:static + ; + +run any_executor.cpp : target-name no_rtti_any_executor ; diff --git a/test/no_rtti/any_executor.cpp b/test/no_rtti/any_executor.cpp new file mode 100644 index 000000000..d65bbd2d7 --- /dev/null +++ b/test/no_rtti/any_executor.cpp @@ -0,0 +1,123 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// 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) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that header file is self-contained. +#include + +#include +#include + +#include "test_suite.hpp" + +/* This file is built with RTTI disabled (-fno-rtti, /GR-). It proves + that the type-erased executors identify types through detail::type_id + and not through typeid. The #error below fails the build if the flag + did not take effect, so a passing run cannot mean RTTI was on. + + Only executor types local to this file are type-erased here. Under + CMake the capy library is built once, with RTTI on, and detail::type_id + has a different definition in each mode. Instantiating any_executor or + executor_ref for a library type such as thread_pool::executor_type in + this TU would therefore be an ODR violation against the library. +*/ + +#if !BOOST_CAPY_NO_RTTI +# error "test/no_rtti must be compiled with RTTI disabled" +#endif + +namespace boost { +namespace capy { + +namespace { + +// N only makes distinct types. ctx is never dereferenced: context() +// is instantiated by the type erasure but never called, so no +// execution_context object (which lives in the library) is needed. +template +struct local_executor +{ + int id = 0; + execution_context* ctx = nullptr; + + bool operator==(local_executor const& other) const noexcept + { + return id == other.id; + } + execution_context& context() const noexcept { return *ctx; } + void on_work_started() const noexcept {} + void on_work_finished() const noexcept {} + std::coroutine_handle<> dispatch(continuation& c) const { return c.h; } + void post(continuation&) const {} +}; + +using exec_a = local_executor<0>; +using exec_b = local_executor<1>; + +static_assert(Executor); +static_assert(Executor); + +} // namespace + +struct any_executor_no_rtti_test +{ + void + testEquality() + { + any_executor a1(exec_a{1}); + any_executor a1_again(exec_a{1}); + any_executor a2(exec_a{2}); + any_executor b1(exec_b{1}); + + BOOST_TEST(a1 == a1_again); + BOOST_TEST(!(a1 == a2)); + // Equal ids, different types: equals() must reject on type + // before comparing values. + BOOST_TEST(!(a1 == b1)); + } + + void + testTarget() + { + any_executor empty; + BOOST_TEST(empty.target() == nullptr); + + any_executor a(exec_a{7}); + any_executor const& ca = a; + BOOST_TEST(a.target() != nullptr); + BOOST_TEST_EQ(a.target()->id, 7); + BOOST_TEST_EQ(ca.target()->id, 7); + BOOST_TEST(a.target() == a.target()); + BOOST_TEST(a.target() == nullptr); + BOOST_TEST(ca.target() == nullptr); + } + + void + testExecutorRef() + { + exec_a ea{3}; + executor_ref r(ea); + BOOST_TEST(r.target() == &ea); + BOOST_TEST(r.target() == nullptr); + } + + void + run() + { + testEquality(); + testTarget(); + testExecutorRef(); + } +}; + +TEST_SUITE( + any_executor_no_rtti_test, + "boost.capy.no_rtti.any_executor"); + +} // capy +} // boost diff --git a/test/unit/ex/any_executor.cpp b/test/unit/ex/any_executor.cpp index 43efe36ba..8d0a1ea2e 100644 --- a/test/unit/ex/any_executor.cpp +++ b/test/unit/ex/any_executor.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Michael Vandeberg // // 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) @@ -262,19 +263,87 @@ struct any_executor_test } void - testTargetType() + testTarget() { - // Empty executor + // Empty: both overloads return nullptr { any_executor ex; - BOOST_TEST(ex.target_type() == typeid(void)); + any_executor const& cex = ex; + BOOST_TEST(ex.target() == nullptr); + BOOST_TEST(cex.target() == nullptr); } - // With executor + // Matching type: points at the stored copy, not the original + { + counting_context ctx; + counting_executor under(ctx); + any_executor ex(under); + any_executor const& cex = ex; + + counting_executor* p = ex.target(); + counting_executor const* cp = cex.target(); + BOOST_TEST(p != nullptr); + BOOST_TEST(p == cp); + BOOST_TEST(p != &under); + BOOST_TEST(*p == under); + } + + // cv-qualified request finds the same object + { + counting_context ctx; + any_executor ex(counting_executor{ctx}); + counting_executor const* p = + ex.target(); + BOOST_TEST(p != nullptr); + BOOST_TEST(p == ex.target()); + } + + // Mismatched type returns nullptr + { + thread_pool pool(1); + any_executor ex(pool.get_executor()); + BOOST_TEST(ex.target() == nullptr); + BOOST_TEST(ex.target() != nullptr); + } + + // A non-executor type compiles and returns nullptr { thread_pool pool(1); any_executor ex(pool.get_executor()); - BOOST_TEST(ex.target_type() == typeid(thread_pool::executor_type)); + any_executor const& cex = ex; + BOOST_TEST(ex.target() == nullptr); + BOOST_TEST(cex.target() == nullptr); + } + + // Same type, different contexts: target() tells them apart + { + thread_pool pool1(1); + thread_pool pool2(1); + any_executor ex1(pool1.get_executor()); + any_executor ex2(pool2.get_executor()); + BOOST_TEST(*ex1.target() == + pool1.get_executor()); + BOOST_TEST(*ex2.target() == + pool2.get_executor()); + } + + // Copies share one stored executor + { + counting_context ctx; + any_executor ex1(counting_executor{ctx}); + any_executor ex2 = ex1; + BOOST_TEST(ex1.target() == + ex2.target()); + } + + // Copies share: a write through one is seen by all + { + counting_context ctx1; + counting_context ctx2; + any_executor ex1(counting_executor{ctx1}); + any_executor ex2 = ex1; + ex1.target()->ctx_ = &ctx2; + BOOST_TEST(&ex2.context() == &ctx2); } } @@ -406,7 +475,7 @@ struct any_executor_test testMove(); testEquality(); testWorkTracking(); - testTargetType(); + testTarget(); testContext(); testDispatch(); testPost(); diff --git a/test/unit/ex/executor_ref.cpp b/test/unit/ex/executor_ref.cpp index 7880d1826..abf7724d9 100644 --- a/test/unit/ex/executor_ref.cpp +++ b/test/unit/ex/executor_ref.cpp @@ -333,6 +333,21 @@ struct executor_ref_test BOOST_TEST_EQ( cex2.target(), nullptr); + + // Empty: both overloads return nullptr + executor_ref empty; + executor_ref const& cempty = empty; + BOOST_TEST_EQ( + empty.target(), + nullptr); + BOOST_TEST_EQ( + cempty.target(), + nullptr); + + static_assert(noexcept( + empty.target())); + static_assert(noexcept( + cempty.target())); } void