From c105c75eb52ea62fb3989a101fb4e37de895e9ca Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Wed, 2 Apr 2025 12:46:35 -0400 Subject: [PATCH 01/20] Enable polymorphic shared_ptr downcasting in Any using type traits - Introduced `any_cast_base` trait to register base types for polymorphic casting. - Added `is_shared_ptr` and `IsPolymorphicSharedPtr` traits to detect eligible types. - Modified `Any` to store shared_ptr as its registered base class if available. - Enhanced castPtr() and tryCast() to support safe downcasting from stored base class to derived class using static or dynamic pointer casting. - Ensures shared_ptr can be stored and retrieved as shared_ptr when registered. --- include/behaviortree_cpp/utils/safe_any.hpp | 154 +++++++++++++++++++- 1 file changed, 152 insertions(+), 2 deletions(-) diff --git a/include/behaviortree_cpp/utils/safe_any.hpp b/include/behaviortree_cpp/utils/safe_any.hpp index 999e7d526..0a85c80cd 100644 --- a/include/behaviortree_cpp/utils/safe_any.hpp +++ b/include/behaviortree_cpp/utils/safe_any.hpp @@ -28,9 +28,25 @@ namespace BT { - static std::type_index UndefinedAnyType = typeid(nullptr); +template +struct any_cast_base +{ + using type = void; // Default: no base known, fallback to default any storage +}; + +// Trait to detect std::shared_ptr types. +template +struct is_shared_ptr : std::false_type +{ +}; + +template +struct is_shared_ptr> : std::true_type +{ +}; + // Rational: since type erased numbers will always use at least 8 bytes // it is faster to cast everything to either double, uint64_t or int64_t. class Any @@ -58,6 +74,32 @@ class Any typename std::enable_if::value && !std::is_enum::value && !std::is_same::value>::type*; + // Helper: IsPolymorphicSharedPtr is true if T is a shared pointer, + // its element_type exists, is polymorphic, and any_cast_base for that element + // is specialized (i.e. not void). + template + struct IsPolymorphicSharedPtr : std::false_type + { + }; + + template + struct IsPolymorphicSharedPtr> + : std::integral_constant< + bool, + is_shared_ptr::value && std::is_polymorphic_v && + !std::is_same_v::type, + void>> + { + }; + + template + using EnablePolymorphicSharedPtr = + std::enable_if_t::value, int*>; + + template + using EnableNonPolymorphicSharedPtr = + std::enable_if_t::value, int*>; + template nonstd::expected stringToNumber() const; @@ -107,6 +149,26 @@ class Any Any(const std::type_index& type) : _original_type(type) {} + // default for shared pointers + template + explicit Any(const std::shared_ptr& value) + : _original_type(typeid(std::shared_ptr)) + { + using Base = typename any_cast_base::type; + + // store as base class if specialized + if constexpr(!std::is_same_v) + { + static_assert(std::is_polymorphic_v, "Any Base trait specialization must be " + "polymorphic"); + _any = std::static_pointer_cast(value); + } + else + { + _any = value; + } + } + // default for other custom types template explicit Any(const T& value, EnableNonIntegral = 0) @@ -158,7 +220,7 @@ class Any // Method to access the value by pointer. // It will return nullptr, if the user try to cast it to a // wrong type or if Any was empty. - template + template > [[nodiscard]] T* castPtr() { static_assert(!std::is_same_v, "The value has been casted internally to " @@ -192,6 +254,56 @@ class Any return _any.empty() ? nullptr : linb::any_cast(&_any); } + // Specialized version of castPtr() for shared_ptr where T is a polymorphic type + // with a registered base class via any_cast_base. + // + // Returns a raw pointer to T::element_type (i.e., Derived*), or nullptr on failure. + // + // Note: This function intentionally does not return a std::shared_ptr* because doing so + // would expose the internal ownership mechanism, which: + // - Breaks encapsulation and may lead to accidental misuse (e.g., double-deletion, ref count tampering) + // - Offers no real benefit, as the purpose of this function is to provide access + // to the managed object, not the smart pointer itself. + // + // By returning a raw pointer to the object, we preserve ownership semantics and safely + // allow read-only access without affecting the reference count. + template > + [[nodiscard]] typename T::element_type* castPtr() + { + using Derived = typename T::element_type; + using Base = typename any_cast_base::type; + + try + { + // Attempt to retrieve the stored shared_ptr from the Any container + auto base_ptr = linb::any_cast>(&_any); + if(!base_ptr) + return nullptr; + + // Case 1: If Base and Derived are the same, no casting is needed + if constexpr(std::is_same_v) + { + return base_ptr ? base_ptr->get() : nullptr; + } + + // Case 2: If the original stored type was shared_ptr, we can safely static_cast + if(_original_type == typeid(std::shared_ptr)) + { + return std::static_pointer_cast(*base_ptr).get(); + } + + // Case 3: Otherwise, attempt a dynamic cast from Base to Derived + auto derived_ptr = std::dynamic_pointer_cast(*base_ptr); + return derived_ptr ? derived_ptr.get() : nullptr; + } + catch(...) + { + return nullptr; + } + + return nullptr; + } + // This is the original type [[nodiscard]] const std::type_index& type() const noexcept { @@ -513,6 +625,44 @@ inline nonstd::expected Any::tryCast() const throw std::runtime_error("Any::cast failed because it is empty"); } + // special case: T is a shared_ptr to a registered polymorphic type. + // The stored value is a shared_ptr, but the user is requesting shared_ptr. + // Perform safe downcasting (static or dynamic) from Base to Derived if applicable. + if constexpr(is_shared_ptr::value) + { + using Derived = typename T::element_type; + using Base = typename any_cast_base::type; + + if constexpr(std::is_polymorphic_v && !std::is_same_v) + { + // Attempt to retrieve the stored shared_ptr from the Any container + auto base_ptr = linb::any_cast>(_any); + if(!base_ptr) + { + throw std::runtime_error("Any::cast cannot cast to shared_ptr class"); + } + + // Case 1: If Base and Derived are the same, no casting is needed + if constexpr(std::is_same_v>) + { + return base_ptr; + } + + // Case 2: If the original stored type was shared_ptr, we can safely static_cast + if(_original_type == typeid(std::shared_ptr)) + { + return std::static_pointer_cast(base_ptr); + } + + // Case 3: Otherwise, attempt a dynamic cast from Base to Derived + auto derived_ptr = std::dynamic_pointer_cast(base_ptr); + if(!derived_ptr) + throw std::runtime_error("Any::cast Dynamic cast failed, types are not related"); + + return derived_ptr; + } + } + if(castedType() == typeid(T)) { return linb::any_cast(_any); From 14815b5fb9973e3288bfeda62f306dbfbb78feb8 Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Wed, 2 Apr 2025 12:10:44 -0400 Subject: [PATCH 02/20] Enable blackboard matching for types sharing a common base class --- include/behaviortree_cpp/basic_types.h | 19 +++++++++++++++++-- include/behaviortree_cpp/blackboard.h | 6 ++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/include/behaviortree_cpp/basic_types.h b/include/behaviortree_cpp/basic_types.h index bceef4fd9..58e852999 100644 --- a/include/behaviortree_cpp/basic_types.h +++ b/include/behaviortree_cpp/basic_types.h @@ -350,6 +350,21 @@ class TypeInfo template static TypeInfo Create() { + // store the base class typeid if specialized + if constexpr(is_shared_ptr::value) + { + using Elem = typename T::element_type; + using Base = typename any_cast_base::type; + + if constexpr(!std::is_same_v) + { + static_assert(std::is_polymorphic_v, "TypeInfo Base trait specialization " + "must be " + "polymorphic"); + return TypeInfo{ typeid(std::shared_ptr), + GetAnyFromStringFunctor>() }; + } + } return TypeInfo{ typeid(T), GetAnyFromStringFunctor() }; } @@ -452,7 +467,8 @@ template } else { - out = { sname, PortInfo(direction, typeid(T), GetAnyFromStringFunctor()) }; + auto type_info = TypeInfo::Create(); + out = { sname, PortInfo(direction, type_info.type(), type_info.converter()) }; } if(!description.empty()) { @@ -501,7 +517,6 @@ BidirectionalPort(StringView name, StringView description = {}) namespace details { - template [[nodiscard]] inline std::pair PortWithDefault(PortDirection direction, StringView name, const DefaultT& default_value, diff --git a/include/behaviortree_cpp/blackboard.h b/include/behaviortree_cpp/blackboard.h index 1c3aa96c6..26a80b7de 100644 --- a/include/behaviortree_cpp/blackboard.h +++ b/include/behaviortree_cpp/blackboard.h @@ -13,7 +13,6 @@ namespace BT { - /// This type contains a pointer to Any, protected /// with a locked mutex as long as the object is in scope using AnyPtrLocked = LockedPtr; @@ -257,8 +256,11 @@ inline void Blackboard::set(const std::string& key, const T& value) std::type_index previous_type = entry.info.type(); + // allow matching if any is of the same base + const auto current_type = TypeInfo::Create().type(); + // check type mismatch - if(previous_type != std::type_index(typeid(T)) && previous_type != new_value.type()) + if(previous_type != current_type && previous_type != new_value.type()) { bool mismatching = true; if(std::is_constructible::value) From 61833403019dc27de975ba75fe60949e829a22ed Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Tue, 1 Apr 2025 20:53:34 -0400 Subject: [PATCH 03/20] Add test fixture for Greeter type hierarchy and type trait registration --- tests/include/greeter_test.h | 195 +++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 tests/include/greeter_test.h diff --git a/tests/include/greeter_test.h b/tests/include/greeter_test.h new file mode 100644 index 000000000..d3f045402 --- /dev/null +++ b/tests/include/greeter_test.h @@ -0,0 +1,195 @@ +#ifndef GREETER_TEST_H_ +#define GREETER_TEST_H_ + +#include + +/** + * +-------------------+------------+-------------+-----------------------------+ + * | Class | Base Class | Polymorphic | Type Trait Registered | + * +-------------------+------------+-------------+-----------------------------+ + * | Greeter | - | Yes | Greeter | + * | HelloGreeter | Greeter | Yes | Greeter | + * | FancyHelloGreeter | Greeter | Yes | Greeter | + * | Unwelcomer | - | Yes | Greeter (Purposely invalid) | + * +-------------------+------------+-------------+-----------------------------+ + */ + +class Greeter +{ +public: + using Ptr = std::shared_ptr; + virtual ~Greeter() = default; + virtual std::string show_msg() const + { + return ""; + }; +}; + +class HelloGreeter : public Greeter +{ +public: + using Ptr = std::shared_ptr; + std::string show_msg() const override + { + return "hello"; + } + void setDerivedParameter(int n){}; +}; + +class FancyHelloGreeter : public HelloGreeter +{ +public: + using Ptr = std::shared_ptr; + std::string show_msg() const override + { + return "salutations"; + } +}; + +class Unwelcomer +{ +public: + using Ptr = std::shared_ptr; + virtual ~Unwelcomer() = default; + virtual std::string show_msg() const + { + return "You’re not welcome here"; + }; +}; + +// Register cast base type for self to allow direct cast +template <> +struct BT::any_cast_base +{ + using type = Greeter; +}; + +// Register cast base type for HelloGreeter +template <> +struct BT::any_cast_base +{ + using type = Greeter; +}; + +// Register cast base type for FancyHelloGreeter +template <> +struct BT::any_cast_base +{ + using type = Greeter; +}; + +// Register cast base type for Unwelcomer +// WARNING: intentionally incorrect +template <> +struct BT::any_cast_base +{ + using type = Greeter; +}; + +/** +* +-------------------+--------------+-------------+-----------------------+ +* | Class | Base Class | Polymorphic | Type Trait Registered | +* +-------------------+--------------+-------------+-----------------------+ +* | GreeterNoReg | - | Yes | - | +* | HelloGreeterNoReg | GreeterNoReg | Yes | - | +* +-------------------+--------------+-------------+-----------------------+ +*/ + +class GreeterNoReg +{ +public: + using Ptr = std::shared_ptr; + virtual ~GreeterNoReg() = default; + virtual std::string show_msg() const + { + return ""; + }; +}; + +class HelloGreeterNoReg : public GreeterNoReg +{ +public: + using Ptr = std::shared_ptr; + std::string show_msg() const override + { + return "hello"; + } + void setDerivedParameter(int n){}; +}; + +/** +* +--------------------+---------------+-------------+-----------------------+ +* | Class | Base Class | Polymorphic | Type Trait Registered | +* +--------------------+---------------+-------------+-----------------------+ +* | GreeterNoPoly | - | No | GreeterNoPoly | +* | HelloGreeterNoPoly | GreeterNoPoly | No | GreeterNoPoly | +* +-----------------------+------------+-------------+-----------------------+ +*/ + +// Correctly fails to compile: +// static_assert(std::is_polymorphic_v, "Base must be polymorphic"); + +// class GreeterNoPoly +// { +// public: +// using Ptr = std::shared_ptr; +// std::string greet() const +// { +// return ""; +// }; +// }; +// +// class HelloGreeterNoPoly : public GreeterNoPoly +// { +// public: +// using Ptr = std::shared_ptr; +// std::string hello_greet() +// { +// return "hello" + greet(); +// } +// }; +// +// // Register cast base type for self to allow direct cast +// template <> +// struct BT::any_cast_base +// { +// using type = GreeterNoPoly; +// }; +// +// // Register cast base type for HelloGreeter +// template <> +// struct BT::any_cast_base +// { +// using type = GreeterNoPoly; +// }; + +/** +* +-----------------------+------------+-------------+-----------------------+ +* | Class | Base Class | Polymorphic | Type Trait Registered | +* +-----------------------+------------+-------------+-----------------------+ +* | GreeterNoPolyReg | - | No | - | +* | HelloGreeterNoPolyReg | NoGreeter | No | - | +* +-----------------------+------------+-------------+-----------------------+ +*/ + +class GreeterNoPolyReg +{ +public: + using Ptr = std::shared_ptr; + std::string greet() const + { + return ""; + }; +}; + +class HelloGreeterNoPolyReg : public GreeterNoPolyReg +{ +public: + using Ptr = std::shared_ptr; + std::string hello_greet() + { + return "hello" + greet(); + } +}; + +#endif // GREETER_TEST_H_ From d644f67ddde3e04b231014f5bf6a35a408780aff Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Tue, 1 Apr 2025 21:09:22 -0400 Subject: [PATCH 04/20] Add unit test for Any/Blackboard type casting --- tests/gtest_any.cpp | 78 ++++++++++++++++++++++++++ tests/gtest_blackboard.cpp | 112 +++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/tests/gtest_any.cpp b/tests/gtest_any.cpp index 395551fdb..dc0575946 100644 --- a/tests/gtest_any.cpp +++ b/tests/gtest_any.cpp @@ -17,6 +17,7 @@ #include #include +#include "greeter_test.h" using namespace BT; @@ -249,4 +250,81 @@ TEST(Any, Cast) Any a(v); EXPECT_EQ(a.cast>(), v); } + + /// Issue 943 + // Type casting: polymorphic class w/ registered base class + { + auto g = std::make_shared(); + Any any_g(g); + EXPECT_NO_THROW(auto res = any_g.cast()); + EXPECT_ANY_THROW(auto res = any_g.cast()); + EXPECT_ANY_THROW(auto res = any_g.cast()); + EXPECT_TRUE(any_g.castPtr()); + EXPECT_FALSE(any_g.castPtr()); + EXPECT_FALSE(any_g.castPtr()); + + auto hg = std::make_shared(); + Any any_hg(hg); + EXPECT_NO_THROW(auto res = any_hg.cast()); + EXPECT_NO_THROW(auto res = any_hg.cast()); + EXPECT_ANY_THROW(auto res = any_hg.cast()); + EXPECT_TRUE(any_hg.castPtr()); + EXPECT_TRUE(any_hg.castPtr()); + EXPECT_FALSE(any_hg.castPtr()); + + auto fhg = std::make_shared(); + Any any_fhg(fhg); + EXPECT_NO_THROW(auto res = any_fhg.cast()); + EXPECT_NO_THROW(auto res = any_fhg.cast()); + EXPECT_NO_THROW(auto res = any_fhg.cast()); + EXPECT_TRUE(any_fhg.castPtr()); + EXPECT_TRUE(any_fhg.castPtr()); + EXPECT_TRUE(any_fhg.castPtr()); + + // Try to upcast to an incorrectly registered base + auto u = std::make_shared(); + + // OK, fails to compile -> invalid static cast + // Any any_u(u); + // EXPECT_ANY_THROW(auto res = any_g.cast()); + } + + // Type casting: polymorphic class w/o registered base class + { + auto g = std::make_shared(); + Any any_g(g); + EXPECT_NO_THROW(auto res = any_g.cast()); + EXPECT_ANY_THROW(auto res = any_g.cast()); + EXPECT_TRUE(any_g.castPtr()); + EXPECT_FALSE(any_g.castPtr()); + + auto hg = std::make_shared(); + Any any_hg(hg); + EXPECT_ANY_THROW(auto res = any_hg.cast()); + EXPECT_NO_THROW(auto res = any_hg.cast()); + EXPECT_FALSE(any_hg.castPtr()); + EXPECT_TRUE(any_hg.castPtr()); + } + + // Type casting: non polymorphic class w/ registered base class + { + // OK: static_assert(std::is_polymorphic_v, "Base must be polymorphic") + } + + // Type casting: non polymorphic class w/o registered base class + { + auto g = std::make_shared(); + Any any_g(g); + EXPECT_NO_THROW(auto res = any_g.cast()); + EXPECT_ANY_THROW(auto res = any_g.cast()); + EXPECT_TRUE(any_g.castPtr()); + EXPECT_FALSE(any_g.castPtr()); + + auto hg = std::make_shared(); + Any any_hg(hg); + EXPECT_ANY_THROW(auto res = any_hg.cast()); + EXPECT_NO_THROW(auto res = any_hg.cast()); + EXPECT_FALSE(any_hg.castPtr()); + EXPECT_TRUE(any_hg.castPtr()); + } } diff --git a/tests/gtest_blackboard.cpp b/tests/gtest_blackboard.cpp index 35be37dab..1744e7cd9 100644 --- a/tests/gtest_blackboard.cpp +++ b/tests/gtest_blackboard.cpp @@ -15,6 +15,7 @@ #include "behaviortree_cpp/blackboard.h" #include "../sample_nodes/dummy_nodes.h" +#include "greeter_test.h" using namespace BT; @@ -854,3 +855,114 @@ TEST(BlackboardTest, SetBlackboard_WithPortRemapping) // Tick till the end with no crashes ASSERT_NO_THROW(tree.tickWhileRunning();); } + + +class CreateHelloGreeter : public SyncActionNode +{ +public: + CreateHelloGreeter(const std::string& name, const NodeConfig& config) + : SyncActionNode(name, config) + {} + + NodeStatus tick() override + { + auto hello_greeter = std::make_shared(); + + setOutput("out_derived", hello_greeter); + + return NodeStatus::SUCCESS; + } + + static PortsList providedPorts() + { + return { BT::OutputPort("out_derived") }; + } +}; + +class SetDerivedParameter : public SyncActionNode +{ +public: + SetDerivedParameter(const std::string& name, const NodeConfig& config) + : SyncActionNode(name, config) + {} + + NodeStatus tick() override + { + int n; + HelloGreeter::Ptr hello_greeter{}; + + getInput("n", n); + getInput("in_derived", hello_greeter); + + hello_greeter->setDerivedParameter(n); + hello_greeter->show_msg(); + + return NodeStatus::SUCCESS; + } + + static PortsList providedPorts() + { + return { BT::InputPort("n"), BT::InputPort("in_derived") }; + } +}; + +class ShowGreetMessage : public SyncActionNode +{ +public: + ShowGreetMessage(const std::string& name, const NodeConfig& config) + : SyncActionNode(name, config) + {} + + NodeStatus tick() override + { + Greeter::Ptr greeter{}; + + getInput("in_base", greeter); + greeter->show_msg(); + + return NodeStatus::SUCCESS; + } + + static PortsList providedPorts() + { + return { BT::InputPort("in_base") }; + } +}; + +TEST(BlackboardTest, Upcasting_Issue943) +{ + auto bb = BT::Blackboard::create(); + + auto hello_greeter = std::make_shared(); + bb->set("hello_greeter", hello_greeter); + + std::shared_ptr g{}; + ASSERT_TRUE(bb->get("hello_greeter", g)); + ASSERT_STREQ("hello", g->show_msg().c_str()); + + std::shared_ptr hg{}; + ASSERT_TRUE(bb->get("hello_greeter", hg)); + ASSERT_STREQ("hello", hg->show_msg().c_str()); + + std::string xml_txt = R"( + + + + + + + + + )"; + + BehaviorTreeFactory factory; + factory.registerNodeType("CreateHelloGreeter"); + factory.registerNodeType("SetDerivedParameter"); + factory.registerNodeType("ShowGreetMessage"); + + auto tree = factory.createTreeFromText(xml_txt); + + NodeStatus status = tree.tickWhileRunning(); + + ASSERT_EQ(status, NodeStatus::SUCCESS); +} From f403fb654b3867e870d140f04065082db74b29e7 Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Wed, 2 Apr 2025 21:28:44 -0400 Subject: [PATCH 05/20] Avoid std::is_polymorphic instantiation on incomplete types Added is_polymorphic_safe to defer evaluation of std::is_polymorphic until T is known to be a complete type. This prevents compilation errors when used with forward-declared types. --- include/behaviortree_cpp/basic_types.h | 2 +- include/behaviortree_cpp/utils/safe_any.hpp | 43 +++++++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/include/behaviortree_cpp/basic_types.h b/include/behaviortree_cpp/basic_types.h index 58e852999..002d0a33f 100644 --- a/include/behaviortree_cpp/basic_types.h +++ b/include/behaviortree_cpp/basic_types.h @@ -358,7 +358,7 @@ class TypeInfo if constexpr(!std::is_same_v) { - static_assert(std::is_polymorphic_v, "TypeInfo Base trait specialization " + static_assert(is_polymorphic_safe_v, "TypeInfo Base trait specialization " "must be " "polymorphic"); return TypeInfo{ typeid(std::shared_ptr), diff --git a/include/behaviortree_cpp/utils/safe_any.hpp b/include/behaviortree_cpp/utils/safe_any.hpp index 0a85c80cd..d998680ff 100644 --- a/include/behaviortree_cpp/utils/safe_any.hpp +++ b/include/behaviortree_cpp/utils/safe_any.hpp @@ -47,6 +47,33 @@ struct is_shared_ptr> : std::true_type { }; +// Trait to detect if a type is complete +template +struct is_complete : std::false_type +{ +}; + +template +struct is_complete : std::true_type +{ +}; + +// Trait to detect if a trait is complete and polymorphic +template +struct is_polymorphic_safe : std::false_type +{ +}; + +// Specialization only enabled if T is complete +template +struct is_polymorphic_safe::value>> + : std::integral_constant::value> +{ +}; + +template +inline constexpr bool is_polymorphic_safe_v = is_polymorphic_safe::value; + // Rational: since type erased numbers will always use at least 8 bytes // it is faster to cast everything to either double, uint64_t or int64_t. class Any @@ -83,12 +110,12 @@ class Any }; template - struct IsPolymorphicSharedPtr> - : std::integral_constant< - bool, - is_shared_ptr::value && std::is_polymorphic_v && - !std::is_same_v::type, - void>> + struct IsPolymorphicSharedPtr< + T, + std::enable_if_t< + is_shared_ptr::value && is_polymorphic_safe_v && + !std::is_same_v::type, void>>> + : std::true_type { }; @@ -159,7 +186,7 @@ class Any // store as base class if specialized if constexpr(!std::is_same_v) { - static_assert(std::is_polymorphic_v, "Any Base trait specialization must be " + static_assert(is_polymorphic_safe_v, "Any Base trait specialization must be " "polymorphic"); _any = std::static_pointer_cast(value); } @@ -633,7 +660,7 @@ inline nonstd::expected Any::tryCast() const using Derived = typename T::element_type; using Base = typename any_cast_base::type; - if constexpr(std::is_polymorphic_v && !std::is_same_v) + if constexpr(is_polymorphic_safe_v && !std::is_same_v) { // Attempt to retrieve the stored shared_ptr from the Any container auto base_ptr = linb::any_cast>(_any); From 5829a40a266d6fda7046096081faed991db1125f Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Fri, 4 Apr 2025 16:57:21 -0400 Subject: [PATCH 06/20] Refactor castPtr() to unify shared_ptr casting logic Introduced _cached_derived_ptr to temporarily store downcasted shared_ptr results which are polymorphic and base-registered. --- include/behaviortree_cpp/utils/safe_any.hpp | 116 ++++++++------------ 1 file changed, 45 insertions(+), 71 deletions(-) diff --git a/include/behaviortree_cpp/utils/safe_any.hpp b/include/behaviortree_cpp/utils/safe_any.hpp index d998680ff..1615c5ceb 100644 --- a/include/behaviortree_cpp/utils/safe_any.hpp +++ b/include/behaviortree_cpp/utils/safe_any.hpp @@ -101,32 +101,6 @@ class Any typename std::enable_if::value && !std::is_enum::value && !std::is_same::value>::type*; - // Helper: IsPolymorphicSharedPtr is true if T is a shared pointer, - // its element_type exists, is polymorphic, and any_cast_base for that element - // is specialized (i.e. not void). - template - struct IsPolymorphicSharedPtr : std::false_type - { - }; - - template - struct IsPolymorphicSharedPtr< - T, - std::enable_if_t< - is_shared_ptr::value && is_polymorphic_safe_v && - !std::is_same_v::type, void>>> - : std::true_type - { - }; - - template - using EnablePolymorphicSharedPtr = - std::enable_if_t::value, int*>; - - template - using EnableNonPolymorphicSharedPtr = - std::enable_if_t::value, int*>; - template nonstd::expected stringToNumber() const; @@ -247,7 +221,10 @@ class Any // Method to access the value by pointer. // It will return nullptr, if the user try to cast it to a // wrong type or if Any was empty. - template > + // + // WARNING: The returned pointer may alias internal cache and be invalidated by subsequent castPtr() calls. + // Do not store it long-term. Applies only to shared_ptr where Derived is polymorphic and base-registered. + template [[nodiscard]] T* castPtr() { static_assert(!std::is_same_v, "The value has been casted internally to " @@ -278,57 +255,53 @@ class Any "tea" "d"); - return _any.empty() ? nullptr : linb::any_cast(&_any); - } - - // Specialized version of castPtr() for shared_ptr where T is a polymorphic type - // with a registered base class via any_cast_base. - // - // Returns a raw pointer to T::element_type (i.e., Derived*), or nullptr on failure. - // - // Note: This function intentionally does not return a std::shared_ptr* because doing so - // would expose the internal ownership mechanism, which: - // - Breaks encapsulation and may lead to accidental misuse (e.g., double-deletion, ref count tampering) - // - Offers no real benefit, as the purpose of this function is to provide access - // to the managed object, not the smart pointer itself. - // - // By returning a raw pointer to the object, we preserve ownership semantics and safely - // allow read-only access without affecting the reference count. - template > - [[nodiscard]] typename T::element_type* castPtr() - { - using Derived = typename T::element_type; - using Base = typename any_cast_base::type; - - try + // Special case: applies only when requesting shared_ptr and Derived is polymorphic + // with a registered base via any_cast_base. + if constexpr(is_shared_ptr::value) { - // Attempt to retrieve the stored shared_ptr from the Any container - auto base_ptr = linb::any_cast>(&_any); - if(!base_ptr) - return nullptr; + using Derived = typename T::element_type; + using Base = typename any_cast_base::type; - // Case 1: If Base and Derived are the same, no casting is needed - if constexpr(std::is_same_v) + if constexpr(is_polymorphic_safe_v && !std::is_same_v) { - return base_ptr ? base_ptr->get() : nullptr; - } + try + { + // Attempt to retrieve the stored shared_ptr from the Any container + auto base_ptr = linb::any_cast>(&_any); + if(!base_ptr) + return nullptr; + + // Case 1: If Base and Derived are the same, no casting is needed + if constexpr(std::is_same_v) + { + return reinterpret_cast(base_ptr); + } + + // Case 2: Originally stored as shared_ptr + if(_original_type == typeid(std::shared_ptr)) + { + _cached_derived_ptr = std::static_pointer_cast(*base_ptr); + return reinterpret_cast(&_cached_derived_ptr); + } + + // Case 3: Fallback to dynamic cast + auto derived_ptr = std::dynamic_pointer_cast(*base_ptr); + if(derived_ptr) + { + _cached_derived_ptr = derived_ptr; + return reinterpret_cast(&_cached_derived_ptr); + } + } + catch(...) + { + return nullptr; + } - // Case 2: If the original stored type was shared_ptr, we can safely static_cast - if(_original_type == typeid(std::shared_ptr)) - { - return std::static_pointer_cast(*base_ptr).get(); + return nullptr; } - - // Case 3: Otherwise, attempt a dynamic cast from Base to Derived - auto derived_ptr = std::dynamic_pointer_cast(*base_ptr); - return derived_ptr ? derived_ptr.get() : nullptr; - } - catch(...) - { - return nullptr; } - return nullptr; + return _any.empty() ? nullptr : linb::any_cast(&_any); } // This is the original type @@ -351,6 +324,7 @@ class Any private: linb::any _any; std::type_index _original_type; + mutable std::shared_ptr _cached_derived_ptr = nullptr; //---------------------------- From 4a2e42ce0d92d5d49ef51273b70d980e2cf7e7f6 Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Thu, 17 Apr 2025 17:04:07 -0400 Subject: [PATCH 07/20] Support recursive root base resolution for polymorphic shared_ptr types - Introduced root_base_t to recursively resolve the ultimate base type using the any_cast_base trait. - Applied root_base_t in TypeInfo::Create, Any constructor, castPtr, and tryCast to normalize stored/casted types across polymorphic hierarchies. - Ensured static_assert enforces polymorphic base safety on resolved RootBase. --- include/behaviortree_cpp/basic_types.h | 13 +++-- include/behaviortree_cpp/utils/safe_any.hpp | 63 ++++++++++++++++++--- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/include/behaviortree_cpp/basic_types.h b/include/behaviortree_cpp/basic_types.h index 002d0a33f..94743ca6c 100644 --- a/include/behaviortree_cpp/basic_types.h +++ b/include/behaviortree_cpp/basic_types.h @@ -358,11 +358,14 @@ class TypeInfo if constexpr(!std::is_same_v) { - static_assert(is_polymorphic_safe_v, "TypeInfo Base trait specialization " - "must be " - "polymorphic"); - return TypeInfo{ typeid(std::shared_ptr), - GetAnyFromStringFunctor>() }; + using RootBase = root_base_t; + + static_assert(is_polymorphic_safe_v, "TypeInfo Base trait " + "specialization " + "must be " + "polymorphic"); + return TypeInfo{ typeid(std::shared_ptr), + GetAnyFromStringFunctor>() }; } } return TypeInfo{ typeid(T), GetAnyFromStringFunctor() }; diff --git a/include/behaviortree_cpp/utils/safe_any.hpp b/include/behaviortree_cpp/utils/safe_any.hpp index 1615c5ceb..b3123373d 100644 --- a/include/behaviortree_cpp/utils/safe_any.hpp +++ b/include/behaviortree_cpp/utils/safe_any.hpp @@ -36,6 +36,48 @@ struct any_cast_base using type = void; // Default: no base known, fallback to default any storage }; +// C++17 backport of std::type_identity +template +struct type_identity +{ + using type = T; +}; + +// Trait to check if a type has a valid cast base +template +struct has_valid_cast_base : std::false_type +{ +}; + +template +struct has_valid_cast_base::type>> +{ + static constexpr bool value = + !std::is_same::type, void>::value; +}; + +// Recursive helper (non-self-recursive, SFINAE-safe) +template +struct resolve_root_base_helper +{ + using Base = typename any_cast_base::type; + + using type = typename std::conditional::value, type_identity, + resolve_root_base_helper>::type::type; +}; + +// Public interface with guard +template +struct root_base_resolver +{ + using type = typename std::conditional::value, + resolve_root_base_helper, + type_identity>::type::type; +}; + +template +using root_base_t = typename root_base_resolver::type; + // Trait to detect std::shared_ptr types. template struct is_shared_ptr : std::false_type @@ -160,9 +202,12 @@ class Any // store as base class if specialized if constexpr(!std::is_same_v) { - static_assert(is_polymorphic_safe_v, "Any Base trait specialization must be " - "polymorphic"); - _any = std::static_pointer_cast(value); + using RootBase = root_base_t; + + static_assert(is_polymorphic_safe_v, "Any Base trait specialization must " + "be " + "polymorphic"); + _any = std::static_pointer_cast(value); } else { @@ -264,15 +309,17 @@ class Any if constexpr(is_polymorphic_safe_v && !std::is_same_v) { + using RootBase = root_base_t; + try { // Attempt to retrieve the stored shared_ptr from the Any container - auto base_ptr = linb::any_cast>(&_any); + auto base_ptr = linb::any_cast>(&_any); if(!base_ptr) return nullptr; // Case 1: If Base and Derived are the same, no casting is needed - if constexpr(std::is_same_v) + if constexpr(std::is_same_v) { return reinterpret_cast(base_ptr); } @@ -636,15 +683,17 @@ inline nonstd::expected Any::tryCast() const if constexpr(is_polymorphic_safe_v && !std::is_same_v) { + using RootBase = root_base_t; + // Attempt to retrieve the stored shared_ptr from the Any container - auto base_ptr = linb::any_cast>(_any); + auto base_ptr = linb::any_cast>(_any); if(!base_ptr) { throw std::runtime_error("Any::cast cannot cast to shared_ptr class"); } // Case 1: If Base and Derived are the same, no casting is needed - if constexpr(std::is_same_v>) + if constexpr(std::is_same_v>) { return base_ptr; } From 6f0c367e98782a3f01c39266ed397d71acb7e437 Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Thu, 17 Apr 2025 17:05:51 -0400 Subject: [PATCH 08/20] Adjust any_cast_base to map to HelloGreeter - Reflects the intended design that any_cast_base does not need to map directly to the root base, but can resolve recursively via intermediate types. - Enables fine-grained control over polymorphic casting resolution. --- tests/include/greeter_test.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/include/greeter_test.h b/tests/include/greeter_test.h index d3f045402..41b2a7053 100644 --- a/tests/include/greeter_test.h +++ b/tests/include/greeter_test.h @@ -75,7 +75,7 @@ struct BT::any_cast_base template <> struct BT::any_cast_base { - using type = Greeter; + using type = HelloGreeter; }; // Register cast base type for Unwelcomer From d313becd9f5904fa94f0fa30c443c9cf969f8583 Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Fri, 18 Apr 2025 10:25:55 -0400 Subject: [PATCH 09/20] Add compile-time base class chain introspection using any_cast_base - Introduced type_list to represent type chains at compile time. - Implemented compute_base_chain_impl to recursively collect base types based on any_cast_base specializations. - Added get_base_chain_type_index() to convert the chain into std::vector for runtime use. - Supports safe termination when base is void or self-referential. --- include/behaviortree_cpp/utils/safe_any.hpp | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/include/behaviortree_cpp/utils/safe_any.hpp b/include/behaviortree_cpp/utils/safe_any.hpp index b3123373d..d6736b84e 100644 --- a/include/behaviortree_cpp/utils/safe_any.hpp +++ b/include/behaviortree_cpp/utils/safe_any.hpp @@ -116,6 +116,61 @@ struct is_polymorphic_safe::value>> template inline constexpr bool is_polymorphic_safe_v = is_polymorphic_safe::value; +// Compute and store the base class inheritance chain of a type T +// from derived to root base order, e.g. SphynxCat -> Cat -> Animal +template +struct type_list +{ + template + using prepend = type_list; +}; + +template +std::vector to_type_index_vector(type_list) +{ + return { std::type_index(typeid(Ts))... }; +} + +template +struct compute_base_chain_impl +{ + using type = type_list; +}; + +// Base case: recursion ends when base is void or equal to self +template +struct compute_base_chain_impl< + T, std::enable_if_t::type, void> || + std::is_same_v::type, T>>> +{ + using type = type_list; +}; + +// Recursive case +template +struct compute_base_chain_impl< + T, std::enable_if_t::value && + !std::is_same_v::type, void> && + !std::is_same_v::type, T> && + is_polymorphic_safe_v>> +{ +private: + using Base = typename any_cast_base::type; + using BaseChain = typename compute_base_chain_impl::type; + +public: + using type = typename BaseChain::template prepend; +}; + +template +using compute_base_chain = typename compute_base_chain_impl::type; + +template +std::vector get_base_chain_type_index() +{ + return to_type_index_vector(compute_base_chain{}); +} + // Rational: since type erased numbers will always use at least 8 bytes // it is faster to cast everything to either double, uint64_t or int64_t. class Any From f90f8229e3f71003907738f12fbaf520df504042 Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Fri, 18 Apr 2025 10:26:39 -0400 Subject: [PATCH 10/20] Extend TypeInfo to store and expose base class chain - Modified TypeInfo::Create() to compute and store the full base type chain using compute_base_chain and to_type_index_vector. - Added a new constructor to TypeInfo to accept a base_chain vector. - Introduced baseChain() accessor and isConvertibleFrom() helper. - Enables runtime introspection and safe type conversions across the base chain. --- include/behaviortree_cpp/basic_types.h | 28 ++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/include/behaviortree_cpp/basic_types.h b/include/behaviortree_cpp/basic_types.h index 94743ca6c..5a3992d4e 100644 --- a/include/behaviortree_cpp/basic_types.h +++ b/include/behaviortree_cpp/basic_types.h @@ -364,8 +364,13 @@ class TypeInfo "specialization " "must be " "polymorphic"); - return TypeInfo{ typeid(std::shared_ptr), - GetAnyFromStringFunctor>() }; + + using Chain = compute_base_chain; + auto base_chain = to_type_index_vector(Chain{}); + + return TypeInfo(typeid(std::shared_ptr), + GetAnyFromStringFunctor>(), + std::move(base_chain)); } } return TypeInfo{ typeid(T), GetAnyFromStringFunctor() }; @@ -378,6 +383,14 @@ class TypeInfo : type_info_(type_info), converter_(conv), type_str_(BT::demangle(type_info)) {} + TypeInfo(std::type_index type_info, StringConverter conv, + std::vector&& base_chain) + : type_info_(type_info) + , converter_(conv) + , type_str_(BT::demangle(type_info)) + , base_chain_(std::move(base_chain)) + {} + [[nodiscard]] const std::type_index& type() const; [[nodiscard]] const std::string& typeName() const; @@ -403,10 +416,21 @@ class TypeInfo return converter_; } + [[nodiscard]] bool isConvertibleFrom(const std::type_index& other) const + { + return std::find(base_chain_.begin(), base_chain_.end(), other) != base_chain_.end(); + } + + [[nodiscard]] const std::vector& baseChain() const + { + return base_chain_; + } + private: std::type_index type_info_; StringConverter converter_; std::string type_str_; + std::vector base_chain_; }; class PortInfo : public TypeInfo From 423e970d2b75e029ff331d025bc5fed92c4e9acc Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Fri, 18 Apr 2025 10:28:37 -0400 Subject: [PATCH 11/20] Add unit test for upcasting base type chain in PortInfo --- tests/gtest_ports.cpp | 58 ++++++++++++++++++++++++++++++++++++ tests/include/greeter_test.h | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/tests/gtest_ports.cpp b/tests/gtest_ports.cpp index 69c351594..c1e79c5c7 100644 --- a/tests/gtest_ports.cpp +++ b/tests/gtest_ports.cpp @@ -3,6 +3,7 @@ #include "behaviortree_cpp/bt_factory.h" #include "behaviortree_cpp/xml_parsing.h" #include "behaviortree_cpp/json_export.h" +#include "greeter_test.h" // upcasting tests using namespace BT; @@ -730,3 +731,60 @@ TEST(PortTest, DefaultWronglyOverriden) // This is correct ASSERT_NO_THROW(auto tree = factory.createTreeFromText(xml_txt_correct)); } + +TEST(PortTest, Upcasting_Issue943) +{ + using namespace BT; + + auto plant = PortInfo::Create(); // not registered + auto animal = PortInfo::Create(); + auto cat = PortInfo::Create(); + auto dog = PortInfo::Create(); + auto sphynx = PortInfo::Create(); + + ASSERT_EQ(0, plant.baseChain().size()); + ASSERT_EQ(1, animal.baseChain().size()); + ASSERT_EQ(2, cat.baseChain().size()); + ASSERT_EQ(2, dog.baseChain().size()); + ASSERT_EQ(3, sphynx.baseChain().size()); + + ASSERT_EQ(std::type_index(typeid(Animal)), animal.baseChain()[0]); + ASSERT_EQ(std::type_index(typeid(Cat)), cat.baseChain()[0]); + ASSERT_EQ(std::type_index(typeid(Animal)), cat.baseChain()[1]); + ASSERT_EQ(std::type_index(typeid(Dog)), dog.baseChain()[0]); + ASSERT_EQ(std::type_index(typeid(Animal)), dog.baseChain()[1]); + + ASSERT_EQ(std::type_index(typeid(SphynxCat)), sphynx.baseChain()[0]); + ASSERT_EQ(std::type_index(typeid(Cat)), sphynx.baseChain()[1]); + ASSERT_EQ(std::type_index(typeid(Animal)), sphynx.baseChain()[2]); + + ASSERT_FALSE(plant.isConvertibleFrom(std::type_index(typeid(Plant)))); + ASSERT_FALSE(plant.isConvertibleFrom(std::type_index(typeid(Animal)))); + ASSERT_FALSE(plant.isConvertibleFrom(std::type_index(typeid(Cat)))); + ASSERT_FALSE(plant.isConvertibleFrom(std::type_index(typeid(Dog)))); + ASSERT_FALSE(plant.isConvertibleFrom(std::type_index(typeid(SphynxCat)))); + + ASSERT_FALSE(animal.isConvertibleFrom(std::type_index(typeid(Plant)))); + ASSERT_TRUE(animal.isConvertibleFrom(std::type_index(typeid(Animal)))); + ASSERT_FALSE(animal.isConvertibleFrom(std::type_index(typeid(Cat)))); + ASSERT_FALSE(animal.isConvertibleFrom(std::type_index(typeid(Dog)))); + ASSERT_FALSE(animal.isConvertibleFrom(std::type_index(typeid(SphynxCat)))); + + ASSERT_FALSE(cat.isConvertibleFrom(std::type_index(typeid(Plant)))); + ASSERT_TRUE(cat.isConvertibleFrom(std::type_index(typeid(Animal)))); + ASSERT_TRUE(cat.isConvertibleFrom(std::type_index(typeid(Cat)))); + ASSERT_FALSE(cat.isConvertibleFrom(std::type_index(typeid(Dog)))); + ASSERT_FALSE(cat.isConvertibleFrom(std::type_index(typeid(SphynxCat)))); + + ASSERT_FALSE(dog.isConvertibleFrom(std::type_index(typeid(Plant)))); + ASSERT_TRUE(dog.isConvertibleFrom(std::type_index(typeid(Animal)))); + ASSERT_FALSE(dog.isConvertibleFrom(std::type_index(typeid(Cat)))); + ASSERT_TRUE(dog.isConvertibleFrom(std::type_index(typeid(Dog)))); + ASSERT_FALSE(dog.isConvertibleFrom(std::type_index(typeid(SphynxCat)))); + + ASSERT_FALSE(sphynx.isConvertibleFrom(std::type_index(typeid(Plant)))); + ASSERT_TRUE(sphynx.isConvertibleFrom(std::type_index(typeid(Animal)))); + ASSERT_TRUE(sphynx.isConvertibleFrom(std::type_index(typeid(Cat)))); + ASSERT_FALSE(sphynx.isConvertibleFrom(std::type_index(typeid(Dog)))); + ASSERT_TRUE(sphynx.isConvertibleFrom(std::type_index(typeid(SphynxCat)))); +} diff --git a/tests/include/greeter_test.h b/tests/include/greeter_test.h index 41b2a7053..f452ed4fd 100644 --- a/tests/include/greeter_test.h +++ b/tests/include/greeter_test.h @@ -3,6 +3,62 @@ #include +class Animal +{ +public: + using Ptr = std::shared_ptr; + virtual ~Animal() = default; +}; + +class Cat : public Animal +{ +public: + using Ptr = std::shared_ptr; +}; + +class SphynxCat : public Cat +{ +public: + using Ptr = std::shared_ptr; +}; + +class Dog : public Animal +{ +public: + using Ptr = std::shared_ptr; +}; + +// Not registered +class Plant +{ +public: + using Ptr = std::shared_ptr; +}; + +template <> +struct BT::any_cast_base +{ + using type = Animal; +}; + +template <> +struct BT::any_cast_base +{ + using type = Animal; +}; + +template <> +struct BT::any_cast_base +{ + using type = Cat; +}; + +template <> +struct BT::any_cast_base +{ + using type = Animal; +}; + /** * +-------------------+------------+-------------+-----------------------------+ * | Class | Base Class | Polymorphic | Type Trait Registered | From 8ac0c1cf43f6903e2a01522104ff0838359e8293 Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Fri, 18 Apr 2025 18:06:54 -0400 Subject: [PATCH 12/20] Use original typeid for shared_ptr types in TypeInfo::Create() --- include/behaviortree_cpp/basic_types.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/include/behaviortree_cpp/basic_types.h b/include/behaviortree_cpp/basic_types.h index 5a3992d4e..f63f4f67c 100644 --- a/include/behaviortree_cpp/basic_types.h +++ b/include/behaviortree_cpp/basic_types.h @@ -368,9 +368,7 @@ class TypeInfo using Chain = compute_base_chain; auto base_chain = to_type_index_vector(Chain{}); - return TypeInfo(typeid(std::shared_ptr), - GetAnyFromStringFunctor>(), - std::move(base_chain)); + return TypeInfo(typeid(T), GetAnyFromStringFunctor(), std::move(base_chain)); } } return TypeInfo{ typeid(T), GetAnyFromStringFunctor() }; From 12c1377dd24b62427b3d7b65e383524ab4fbe92b Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Fri, 18 Apr 2025 18:14:27 -0400 Subject: [PATCH 13/20] Add PortInfo constructors that accept TypeInfo for cleaner initialization Simplifies PortInfo creation by directly accepting TypeInfo objects, allowing reuse of TypeInfo::Create() without redundant field extraction. --- include/behaviortree_cpp/basic_types.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/include/behaviortree_cpp/basic_types.h b/include/behaviortree_cpp/basic_types.h index f63f4f67c..b5827247f 100644 --- a/include/behaviortree_cpp/basic_types.h +++ b/include/behaviortree_cpp/basic_types.h @@ -442,6 +442,14 @@ class PortInfo : public TypeInfo : TypeInfo(type_info, conv), direction_(direction) {} + PortInfo(PortDirection direction, const TypeInfo& type_info) + : TypeInfo(type_info), direction_(direction) + {} + + PortInfo(PortDirection direction, TypeInfo&& type_info) + : TypeInfo(std::move(type_info)), direction_(direction) + {} + [[nodiscard]] PortDirection direction() const; void setDescription(StringView description); @@ -493,7 +501,7 @@ template else { auto type_info = TypeInfo::Create(); - out = { sname, PortInfo(direction, type_info.type(), type_info.converter()) }; + out = { sname, PortInfo(direction, std::move(type_info)) }; } if(!description.empty()) { From 42b1d808bcb857c1cfcaef4d5a265d223d64dc52 Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Fri, 18 Apr 2025 18:17:17 -0400 Subject: [PATCH 14/20] Allow port type mismatch if convertible via base class chain Adds support for polymorphic upcasting by checking if the previous type is included in the base_chain of the current port during blackboard type consistency checks. --- src/xml_parsing.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index 885244ed9..978bf867c 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -825,10 +825,19 @@ TreeNode::Ptr XMLParser::PImpl::createNodeFromXML(const XMLElement* element, if(auto prev_info = blackboard->entryInfo(port_key)) { // Check consistency of types. - bool const port_type_mismatch = + bool port_type_mismatch = (prev_info->isStronglyTyped() && port_info.isStronglyTyped() && prev_info->type() != port_info.type()); + // allow mismatch if the previous type appears in the base_chain + // of the current port (i.e., valid polymorphic upcast) + if(port_type_mismatch && !prev_info->baseChain().empty() && + !port_info.baseChain().empty() && + prev_info->isConvertibleFrom(port_info.baseChain()[0])) + { + port_type_mismatch = false; + } + // special case related to convertFromString bool const string_input = (prev_info->type() == typeid(std::string)); From 5090086fb2b5bd7fd8d28808e7732ba9e3de5033 Mon Sep 17 00:00:00 2001 From: Captain Yoshi Date: Fri, 18 Apr 2025 18:32:38 -0400 Subject: [PATCH 15/20] Improve test coverage for polymorphic port casting --- tests/gtest_blackboard.cpp | 128 +++++++++++++++++++++++++++++++------ 1 file changed, 109 insertions(+), 19 deletions(-) diff --git a/tests/gtest_blackboard.cpp b/tests/gtest_blackboard.cpp index 1744e7cd9..67e6c4bd4 100644 --- a/tests/gtest_blackboard.cpp +++ b/tests/gtest_blackboard.cpp @@ -856,7 +856,6 @@ TEST(BlackboardTest, SetBlackboard_WithPortRemapping) ASSERT_NO_THROW(tree.tickWhileRunning();); } - class CreateHelloGreeter : public SyncActionNode { public: @@ -929,22 +928,68 @@ class ShowGreetMessage : public SyncActionNode } }; -TEST(BlackboardTest, Upcasting_Issue943) +class ShowFancyGreetMessage : public SyncActionNode { - auto bb = BT::Blackboard::create(); +public: + ShowFancyGreetMessage(const std::string& name, const NodeConfig& config) + : SyncActionNode(name, config) + {} - auto hello_greeter = std::make_shared(); - bb->set("hello_greeter", hello_greeter); + NodeStatus tick() override + { + FancyHelloGreeter::Ptr greeter{}; + + getInput("in_invalid_derived", greeter); + if(!greeter) + return NodeStatus::FAILURE; - std::shared_ptr g{}; - ASSERT_TRUE(bb->get("hello_greeter", g)); - ASSERT_STREQ("hello", g->show_msg().c_str()); + greeter->show_msg(); - std::shared_ptr hg{}; - ASSERT_TRUE(bb->get("hello_greeter", hg)); - ASSERT_STREQ("hello", hg->show_msg().c_str()); + return NodeStatus::SUCCESS; + } - std::string xml_txt = R"( + static PortsList providedPorts() + { + return { BT::InputPort("in_invalid_derived") }; + } +}; + +TEST(BlackboardTest, Upcasting_Issue943) +{ + { + auto bb = BT::Blackboard::create(); + + // set hello_greeter + auto hello_greeter = std::make_shared(); + bb->set("hello_greeter", hello_greeter); + + // retrieve as base class -> OK + std::shared_ptr g{}; + ASSERT_TRUE(bb->get("hello_greeter", g)); + ASSERT_STREQ("hello", g->show_msg().c_str()); + + // retrieve as derived class -> OK + std::shared_ptr hg{}; + ASSERT_TRUE(bb->get("hello_greeter", hg)); + ASSERT_STREQ("hello", hg->show_msg().c_str()); + + // retrieve as most-derived class -> should throw (type mismatch) + std::shared_ptr fhg{}; + std::cout << "D" << std::endl; + ASSERT_ANY_THROW(auto rc = bb->get("hello_greeter", fhg)); + + // overwrite hello_greeter bb key + ASSERT_ANY_THROW(bb->set("hello_greeter", g)); + ASSERT_NO_THROW(bb->set("hello_greeter", hg)); + ASSERT_ANY_THROW(bb->set("hello_greeter", fhg)); + } + + // This test verifies that polymorphic upcasting works correctly during tree creation. + // The port "hello_greeter" is produced as HelloGreeter and later consumed as both + // HelloGreeter and its base type Greeter. The tree should execute successfully, + // confirming safe polymorphic compatibility through the base_chain mechanism. + { + std::string xml_txt = R"( @@ -955,14 +1000,59 @@ TEST(BlackboardTest, Upcasting_Issue943) )"; - BehaviorTreeFactory factory; - factory.registerNodeType("CreateHelloGreeter"); - factory.registerNodeType("SetDerivedParameter"); - factory.registerNodeType("ShowGreetMessage"); + BehaviorTreeFactory factory; + factory.registerNodeType("CreateHelloGreeter"); + factory.registerNodeType("SetDerivedParameter"); + factory.registerNodeType("ShowGreetMessage"); - auto tree = factory.createTreeFromText(xml_txt); + auto tree = factory.createTreeFromText(xml_txt); - NodeStatus status = tree.tickWhileRunning(); + NodeStatus status = tree.tickWhileRunning(); - ASSERT_EQ(status, NodeStatus::SUCCESS); + ASSERT_EQ(status, NodeStatus::SUCCESS); + } + + // This test ensures that an invalid polymorphic downcast is correctly detected + // during tree creation. The port "hello_greeter" is first created with HelloGreeter, + // then later expected as FancyHelloGreeter (a more derived type), which fails. + { + std::string xml_txt = R"( + + + +