Skip to content

Allow upcasting #944

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: master
Choose a base branch
from

Conversation

captain-yoshi
Copy link

Unit tests with desired behavior from #943.

Any Tests Failures

/home/captain-yoshi/ws/ros/mimik_ws/src/BehaviorTree.CPP/tests/gtest_any.cpp:271: Failure
Expected: auto res = a.cast<std::shared_ptr<Greeter>>() doesn't throw an exception.
  Actual: it throws.
/home/captain-yoshi/ws/ros/mimik_ws/src/BehaviorTree.CPP/tests/gtest_any.cpp:273: Failure
Value of: a.castPtr<std::shared_ptr<Greeter>>()
  Actual: false
Expected: true

Blackboard Test Failure

C++ exception with description "The creation of the tree failed because the port [hello_greeter] was initially created with type [std::shared_ptr<HelloGreeter>] and, later type [std::shared_ptr<Greeter>] was used somewhere else." thrown in the test body.

@captain-yoshi captain-yoshi force-pushed the allow-upcasting branch 3 times, most recently from 8feb137 to 38f9b42 Compare April 2, 2025 18:23
@captain-yoshi
Copy link
Author

There maybe a better way to allow type casting of a polymorphic class.

​This pull request introduces support for upcasting and downcasting std::shared_ptr instances (must be polymorphic and has their base registered) within the Any class, facilitating polymorphic type handling. By leveraging type traits, the Any class can now perform safe and efficient conversions between shared pointers of related types.

  • Introduced any_cast_base<T> 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.

So users who wants this behavior needs to register their classes to it's base class. I don't think we can get away without this process...

// Register cast base type for self to allow direct cast, otherwise defaults to a dynamic cast
template <>
struct BT::any_cast_base<Greeter>
{
  using type = Greeter;
};

// Register cast base type for HelloGreeter
template <>
struct BT::any_cast_base<HelloGreeter>
{
  using type = Greeter;
};

Optionally we could add a macro to simplify this process (not in this PR):

#define BT_REGISTER_BASE_TYPE(Derived, Base)    \
  template <>                                   \
  struct BT::any_cast_base<Derived>             \
  {                                             \
    using type = Base;                          \
  };
  
BT_REGISTER_BASE_TYPE(Greeter, Greeter)
BT_REGISTER_BASE_TYPE(HelloGreeter, Greeter) 

It's a little bit more permissive, then what I intended initially. You can now downcast to something that is invalid (must still be a valid class of the stored base class), e.g. the blackboard will not see a mismatch, but it will fail when trying to retrieve it in a BT::Node.

Regarding the previous point, should we try to downcast early, in the blackboard to stop the tree from running early if it is invalid ? Instead of running the tree and let it fail down the line ?


The Sonarcube check fails, but it does not seem related to this PR.

@captain-yoshi captain-yoshi changed the title [WIP] Allow upcasting Allow upcasting Apr 2, 2025
@captain-yoshi
Copy link
Author

The castPtr function is a little bit tricky when downcasting...

I would prefer solution 2 or 3.

Solution 1

Returns a T::element_type* castPtr() when T is a shared pointer with a polymorphic class and a registered base. For everything else T* is returned.

Pros Cons
No complexity 2 different return types depending on the type.
Cannot retrieve the shared pointer, only access to the raw pointer.

Implemented in 3021ff0

Solution 2

Clean API: T* castPtr() for all types.

Pros Cons
Returns T* Need to store a std_shared<void> member to return a pointer to a Derived class.
Lifetime of pointer not garanteed*, another call to castPtr may change this cached value.

*If getLockedPortContent is used, there is no problem.

Implemented in d11512a

Solution 3

Seperate castPtr function when T is a shared pointer with a polymorphic class and a registered base, e.g. castShared.

Pros Cons
No complexity User has 2 different functions depending on what is stored in the _any.

Not implemented

@facontidavide
Copy link
Collaborator

There is a conflict that need to be resolved. Also, correct me if i am wrong, but isn't this change allowing two ports with derived classes to connect?

For instance, given Cat <- Animal and Dog <- Animal, I would be allowed to connect a Cat to a Dog...

@facontidavide facontidavide self-assigned this Apr 16, 2025
@facontidavide facontidavide requested a review from Copilot April 16, 2025 10:18
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

// Case 1: If Base and Derived are the same, no casting is needed
if constexpr(std::is_same_v<Base, Derived>)
{
return reinterpret_cast<T*>(base_ptr);
Copy link
Preview

Copilot AI Apr 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] When Base and Derived are identical, consider using static_cast instead of reinterpret_cast to improve type safety and clarify conversion intent.

Suggested change
return reinterpret_cast<T*>(base_ptr);
return static_cast<T*>(base_ptr);

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

- Introduced `any_cast_base<T>` 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<Derived> can be stored and retrieved as shared_ptr<Base> when registered.
Added is_polymorphic_safe to defer evaluation of std::is_polymorphic<T>
until T is known to be a complete type. This prevents compilation errors
when used with forward-declared types.
Introduced _cached_derived_ptr to temporarily store downcasted
shared_ptr results which are polymorphic and base-registered.
@captain-yoshi
Copy link
Author

For instance, given Cat <- Animal and Dog <- Animal, I would be allowed to connect a Cat to a Dog...

At the moment yes. If we want to make it fail when creating a Tree node from xml, we would need to store the type id of the base class and the original class, because at this stage, I think the any values are still empty.

But trying to have a SphinxCat into a registered Cat port would also fail because only the the SphinxCat and Animal typeid would be stored when registering SphinxCat. We could fix this by storing a vector (or limit with a max depth) of typeid's from current class to root base class to enable this case.

I would also prefer to make it fail early when trying to connect incompatible types. What wold be the way to go ? Only allow a depth of 2 base classes, allow a max range of depth or allow any depth ? Or something else entirely...

- Introduced root_base_t<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.
- 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.
- Introduced type_list<Ts...> to represent type chains at compile time.
- Implemented compute_base_chain_impl<T> to recursively collect base types
  based on any_cast_base<T> specializations.
- Added get_base_chain_type_index<T>() to convert the chain into
  std::vector<std::type_index> for runtime use.
- Supports safe termination when base is void or self-referential.
- Modified TypeInfo::Create<T>() 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.
@captain-yoshi captain-yoshi force-pushed the allow-upcasting branch 2 times, most recently from e14e74f to fb30816 Compare April 18, 2025 23:11
…tion

Simplifies PortInfo creation by directly accepting TypeInfo objects,
allowing reuse of TypeInfo::Create<T>() without redundant field extraction.
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.
@captain-yoshi

This comment was marked as resolved.

@captain-yoshi
Copy link
Author

I have now replaced the Greeter hierarchy with the Animal one (easier too understand).

I added some new tests. Cat <- Animal and Dog <- Animal now throws when constructing the TreeNode.


I think the last thing missing is the serialization of the TreeNodeModel in XML ? I think we would need to store the chain base, e.g. :

<output_port name="collision_object"
             type="std::shared_ptr&lt;Sphinx&lt;std::allocator&lt;void&gt; &gt; &gt;"
             base_chain="Sphinx;Cat;Animal" />

Do we need something for deserializing the base_chain ? Is this even needed, I got the impression that we always need to register the node type in the BehaviorTreeFactory...

Improves Any::castPtr() by caching results of std::dynamic_pointer_cast to avoid repeated casts.
Adds _cached_type to track the last casted type and reuse the cached pointer.
Also simplifies the case where the requested type matches the root base.
@facontidavide
Copy link
Collaborator

thanks for working on this.

the proposed solution seems to work but it adds some (necessary) complexity and I need to take my time (something that right now I don't have in abundance, to think about alternatives... if they exist.

Please be patient, i will look at this in the next couple of weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants