• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

BehaviorTree / BehaviorTree.CPP / 21672091355

04 Feb 2026 12:49PM UTC coverage: 80.205% (+0.4%) from 79.835%
21672091355

Pull #1107

github

web-flow
Merge 0cb314f5a into c7c0ea78a
Pull Request #1107: Add polymorphic shared_ptr port support (Issue #943)

200 of 220 new or added lines in 7 files covered. (90.91%)

2 existing lines in 1 file now uncovered.

4931 of 6148 relevant lines covered (80.2%)

20983.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.22
/include/behaviortree_cpp/utils/polymorphic_cast_registry.hpp
1
/* Copyright (C) 2022-2025 Davide Faconti -  All Rights Reserved
2
*
3
*   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
4
*   to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
5
*   and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
*   The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
*
8
*   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
9
*   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
10
*   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11
*/
12

13
#pragma once
14

15
#include "behaviortree_cpp/contrib/any.hpp"
16

17
#include <functional>
18
#include <map>
19
#include <memory>
20
#include <mutex>
21
#include <set>
22
#include <typeindex>
23

24
namespace BT
25
{
26

27
/**
28
 * @brief Registry for polymorphic shared_ptr cast relationships.
29
 *
30
 * This enables passing shared_ptr<Derived> to ports expecting shared_ptr<Base>
31
 * without breaking ABI compatibility. Users register inheritance relationships
32
 * at runtime, and the registry handles upcasting/downcasting transparently.
33
 *
34
 * This class is typically owned by BehaviorTreeFactory and passed to Blackboard
35
 * during tree creation. This avoids global state and makes testing easier.
36
 *
37
 * Usage with BehaviorTreeFactory:
38
 *   BehaviorTreeFactory factory;
39
 *   factory.registerPolymorphicCast<Cat, Animal>();
40
 *   factory.registerPolymorphicCast<Sphynx, Cat>();
41
 *   auto tree = factory.createTreeFromText(xml);
42
 */
43
class PolymorphicCastRegistry
44
{
45
public:
46
  using CastFunction = std::function<linb::any(const linb::any&)>;
47

48
  PolymorphicCastRegistry() = default;
299✔
49
  ~PolymorphicCastRegistry() = default;
299✔
50

51
  // Non-copyable, non-movable (contains mutex)
52
  PolymorphicCastRegistry(const PolymorphicCastRegistry&) = delete;
53
  PolymorphicCastRegistry& operator=(const PolymorphicCastRegistry&) = delete;
54
  PolymorphicCastRegistry(PolymorphicCastRegistry&&) = delete;
55
  PolymorphicCastRegistry& operator=(PolymorphicCastRegistry&&) = delete;
56

57
  /**
58
   * @brief Register a Derived -> Base inheritance relationship.
59
   *
60
   * This enables:
61
   * - Upcasting: shared_ptr<Derived> can be retrieved as shared_ptr<Base>
62
   * - Downcasting: shared_ptr<Base> can be retrieved as shared_ptr<Derived>
63
   *   (via dynamic_pointer_cast, may return nullptr if types don't match)
64
   *
65
   * @tparam Derived The derived class (must inherit from Base)
66
   * @tparam Base The base class (must be polymorphic - have virtual functions)
67
   */
68
  template <typename Derived, typename Base>
69
  void registerCast()
15✔
70
  {
71
    static_assert(std::is_base_of_v<Base, Derived>, "Derived must inherit from Base");
72
    static_assert(std::is_polymorphic_v<Base>, "Base must be polymorphic (have virtual "
73
                                               "functions)");
74

75
    std::lock_guard<std::mutex> lock(mutex_);
15✔
76

77
    // Register upcast: Derived -> Base
78
    auto upcast_key = std::make_pair(std::type_index(typeid(std::shared_ptr<Derived>)),
15✔
79
                                     std::type_index(typeid(std::shared_ptr<Base>)));
15✔
80

81
    upcasts_[upcast_key] = [](const linb::any& from) -> linb::any {
26✔
82
      auto ptr = linb::any_cast<std::shared_ptr<Derived>>(from);
11✔
83
      return std::static_pointer_cast<Base>(ptr);
22✔
84
    };
11✔
85

86
    // Register downcast: Base -> Derived (uses dynamic_pointer_cast)
87
    auto downcast_key = std::make_pair(std::type_index(typeid(std::shared_ptr<Base>)),
15✔
88
                                       std::type_index(typeid(std::shared_ptr<Derived>)));
15✔
89

90
    downcasts_[downcast_key] = [](const linb::any& from) -> linb::any {
20✔
91
      auto ptr = linb::any_cast<std::shared_ptr<Base>>(from);
5✔
92
      auto derived = std::dynamic_pointer_cast<Derived>(ptr);
5✔
93
      if(!derived)
5✔
94
      {
95
        throw std::bad_cast();
4✔
96
      }
97
      return derived;
2✔
98
    };
9✔
99

100
    // Track inheritance relationship for port compatibility checks
101
    base_types_[std::type_index(typeid(std::shared_ptr<Derived>))].insert(
30✔
102
        std::type_index(typeid(std::shared_ptr<Base>)));
15✔
103
  }
15✔
104

105
  /**
106
   * @brief Check if from_type can be converted to to_type.
107
   *
108
   * Returns true if:
109
   * - from_type == to_type
110
   * - from_type is a registered derived type of to_type (upcast)
111
   * - to_type is a registered derived type of from_type (downcast)
112
   */
113
  [[nodiscard]] bool isConvertible(std::type_index from_type,
114
                                   std::type_index to_type) const
115
  {
116
    if(from_type == to_type)
117
    {
118
      return true;
119
    }
120

121
    std::lock_guard<std::mutex> lock(mutex_);
122

123
    // Check direct upcast
124
    auto upcast_key = std::make_pair(from_type, to_type);
125
    if(upcasts_.find(upcast_key) != upcasts_.end())
126
    {
127
      return true;
128
    }
129

130
    // Check transitive upcast (e.g., Sphynx -> Cat -> Animal)
131
    if(canUpcastTransitive(from_type, to_type))
132
    {
133
      return true;
134
    }
135

136
    // Check downcast
137
    auto downcast_key = std::make_pair(from_type, to_type);
138
    if(downcasts_.find(downcast_key) != downcasts_.end())
139
    {
140
      return true;
141
    }
142

143
    return false;
144
  }
145

146
  /**
147
   * @brief Check if from_type can be UPCAST to to_type (not downcast).
148
   *
149
   * This is stricter than isConvertible - only allows going from
150
   * derived to base, not the reverse.
151
   */
152
  [[nodiscard]] bool canUpcast(std::type_index from_type, std::type_index to_type) const
8✔
153
  {
154
    if(from_type == to_type)
8✔
155
    {
NEW
156
      return true;
×
157
    }
158

159
    std::lock_guard<std::mutex> lock(mutex_);
8✔
160
    return canUpcastTransitive(from_type, to_type);
8✔
161
  }
8✔
162

163
  /**
164
   * @brief Attempt to cast the value to the target type.
165
   *
166
   * @param from The source any containing a shared_ptr
167
   * @param from_type The type_index of the stored type
168
   * @param to_type The target type_index
169
   * @return The casted any, or empty any if cast fails
170
   */
171
  [[nodiscard]] linb::any tryCast(const linb::any& from, std::type_index from_type,
15✔
172
                                  std::type_index to_type) const
173
  {
174
    if(from_type == to_type)
15✔
175
    {
NEW
176
      return from;
×
177
    }
178

179
    std::lock_guard<std::mutex> lock(mutex_);
15✔
180

181
    // Try direct upcast
182
    auto upcast_key = std::make_pair(from_type, to_type);
15✔
183
    auto upcast_it = upcasts_.find(upcast_key);
15✔
184
    if(upcast_it != upcasts_.end())
15✔
185
    {
186
      try
187
      {
188
        return upcast_it->second(from);
5✔
189
      }
NEW
190
      catch(...)
×
191
      {
NEW
192
        return linb::any{};
×
NEW
193
      }
×
194
    }
195

196
    // Try transitive upcast
197
    auto transitive_result = tryTransitiveUpcast(from, from_type, to_type);
10✔
198
    if(!transitive_result.empty())
10✔
199
    {
200
      return transitive_result;
3✔
201
    }
202

203
    // Try direct downcast
204
    auto downcast_key = std::make_pair(from_type, to_type);
7✔
205
    auto downcast_it = downcasts_.find(downcast_key);
7✔
206
    if(downcast_it != downcasts_.end())
7✔
207
    {
208
      try
209
      {
210
        return downcast_it->second(from);
4✔
211
      }
212
      catch(...)
3✔
213
      {
214
        return linb::any{};
3✔
215
      }
3✔
216
    }
217

218
    // Try transitive downcast
219
    auto transitive_down_result = tryTransitiveDowncast(from, from_type, to_type);
3✔
220
    if(!transitive_down_result.empty())
3✔
221
    {
NEW
222
      return transitive_down_result;
×
223
    }
224

225
    return linb::any{};
3✔
226
  }
15✔
227

228
  /**
229
   * @brief Get all registered base types for a given type.
230
   */
231
  [[nodiscard]] std::set<std::type_index> getBaseTypes(std::type_index type) const
232
  {
233
    std::lock_guard<std::mutex> lock(mutex_);
234
    auto it = base_types_.find(type);
235
    if(it != base_types_.end())
236
    {
237
      return it->second;
238
    }
239
    return {};
240
  }
241

242
  /**
243
   * @brief Clear all registrations (mainly for testing).
244
   */
245
  void clear()
246
  {
247
    std::lock_guard<std::mutex> lock(mutex_);
248
    upcasts_.clear();
249
    downcasts_.clear();
250
    base_types_.clear();
251
  }
252

253
private:
254
  // Check if we can upcast from_type to to_type through a chain of registered casts
255
  [[nodiscard]] bool canUpcastTransitive(std::type_index from_type,
8✔
256
                                         std::type_index to_type) const
257
  {
258
    // BFS to find a path from from_type to to_type
259
    std::set<std::type_index> visited;
8✔
260
    std::vector<std::type_index> queue;
8✔
261
    queue.push_back(from_type);
8✔
262

263
    while(!queue.empty())
15✔
264
    {
265
      auto current = queue.back();
9✔
266
      queue.pop_back();
9✔
267

268
      if(visited.count(current) != 0)
9✔
269
      {
270
        continue;
6✔
271
      }
272
      visited.insert(current);
9✔
273

274
      auto it = base_types_.find(current);
9✔
275
      if(it == base_types_.end())
9✔
276
      {
277
        continue;
6✔
278
      }
279

280
      for(const auto& base : it->second)
4✔
281
      {
282
        if(base == to_type)
3✔
283
        {
284
          return true;
2✔
285
        }
286
        queue.push_back(base);
1✔
287
      }
288
    }
289
    return false;
6✔
290
  }
8✔
291

292
  // Try to perform a transitive upcast
293
  [[nodiscard]] linb::any tryTransitiveUpcast(const linb::any& from,
10✔
294
                                              std::type_index from_type,
295
                                              std::type_index to_type) const
296
  {
297
    // Find path from from_type to to_type using BFS
298
    // Note: std::type_index has no default constructor, so we can't use operator[]
299
    std::map<std::type_index, std::type_index> parent;
10✔
300
    std::vector<std::type_index> queue;
10✔
301
    queue.push_back(from_type);
10✔
302
    parent.insert({ from_type, from_type });
10✔
303

304
    while(!queue.empty())
25✔
305
    {
306
      auto current = queue.back();
18✔
307
      queue.pop_back();
18✔
308

309
      auto it = base_types_.find(current);
18✔
310
      if(it == base_types_.end())
18✔
311
      {
312
        continue;
7✔
313
      }
314

315
      for(const auto& base : it->second)
19✔
316
      {
317
        if(parent.find(base) != parent.end())
11✔
318
        {
NEW
319
          continue;
×
320
        }
321
        parent.insert({ base, current });
11✔
322
        if(base == to_type)
11✔
323
        {
324
          // Found path, reconstruct and apply casts
325
          std::vector<std::type_index> path;
3✔
326
          auto node = to_type;
3✔
327
          while(node != from_type)
9✔
328
          {
329
            path.push_back(node);
6✔
330
            node = parent.at(node);
6✔
331
          }
332
          path.push_back(from_type);
3✔
333
          std::reverse(path.begin(), path.end());
3✔
334

335
          // Apply casts along the path
336
          linb::any current_value = from;
3✔
337
          for(size_t i = 0; i + 1 < path.size(); ++i)
9✔
338
          {
339
            auto cast_key = std::make_pair(path[i], path[i + 1]);
6✔
340
            auto cast_it = upcasts_.find(cast_key);
6✔
341
            if(cast_it == upcasts_.end())
6✔
342
            {
NEW
343
              return linb::any{};
×
344
            }
345
            try
346
            {
347
              current_value = cast_it->second(current_value);
6✔
348
            }
NEW
349
            catch(...)
×
350
            {
NEW
351
              return linb::any{};
×
NEW
352
            }
×
353
          }
354
          return current_value;
3✔
355
        }
3✔
356
        queue.push_back(base);
8✔
357
      }
358
    }
359
    return linb::any{};
7✔
360
  }
10✔
361

362
  // Try to perform a transitive downcast
363
  [[nodiscard]] linb::any tryTransitiveDowncast(const linb::any& from,
3✔
364
                                                std::type_index from_type,
365
                                                std::type_index to_type) const
366
  {
367
    // For downcast, we need to find if to_type has from_type as an ancestor
368
    // Then apply downcasts in reverse order
369
    // Note: std::type_index has no default constructor, so we can't use operator[]
370
    std::map<std::type_index, std::type_index> parent;
3✔
371
    std::vector<std::type_index> queue;
3✔
372
    queue.push_back(to_type);
3✔
373
    parent.insert({ to_type, to_type });
3✔
374

375
    while(!queue.empty())
8✔
376
    {
377
      auto current = queue.back();
6✔
378
      queue.pop_back();
6✔
379

380
      auto it = base_types_.find(current);
6✔
381
      if(it == base_types_.end())
6✔
382
      {
383
        continue;
2✔
384
      }
385

386
      for(const auto& base : it->second)
7✔
387
      {
388
        if(parent.find(base) != parent.end())
4✔
389
        {
NEW
390
          continue;
×
391
        }
392
        parent.insert({ base, current });
4✔
393
        if(base == from_type)
4✔
394
        {
395
          // Found path, reconstruct and apply downcasts
396
          std::vector<std::type_index> path;
1✔
397
          auto node = from_type;
1✔
398
          while(node != to_type)
3✔
399
          {
400
            path.push_back(node);
2✔
401
            node = parent.at(node);
2✔
402
          }
403
          path.push_back(to_type);
1✔
404

405
          // Apply downcasts along the path
406
          linb::any current_value = from;
1✔
407
          for(size_t i = 0; i + 1 < path.size(); ++i)
1✔
408
          {
409
            auto cast_key = std::make_pair(path[i], path[i + 1]);
1✔
410
            auto cast_it = downcasts_.find(cast_key);
1✔
411
            if(cast_it == downcasts_.end())
1✔
412
            {
413
              return linb::any{};
1✔
414
            }
415
            try
416
            {
417
              current_value = cast_it->second(current_value);
1✔
418
            }
419
            catch(...)
1✔
420
            {
421
              return linb::any{};
1✔
422
            }
1✔
423
          }
NEW
424
          return current_value;
×
425
        }
1✔
426
        queue.push_back(base);
3✔
427
      }
428
    }
429
    return linb::any{};
2✔
430
  }
3✔
431

432
  mutable std::mutex mutex_;
433
  std::map<std::pair<std::type_index, std::type_index>, CastFunction> upcasts_;
434
  std::map<std::pair<std::type_index, std::type_index>, CastFunction> downcasts_;
435
  std::map<std::type_index, std::set<std::type_index>> base_types_;
436
};
437

438
}  // namespace BT
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc