Skip to content

Commit e1ed3a1

Browse files
Fix #989: JsonExporter use-after-move in vector converter registration (#1090)
The addConverter(to_json, add_type) method moved `converter` into to_json_converters_ before `vector_converter` captured it, causing std::bad_function_call when converting std::vector<T> to JSON. Fix: create vector_converter before moving converter. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f9a6f98 commit e1ed3a1

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

include/behaviortree_cpp/json_export.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,8 @@ inline void JsonExporter::addConverter(
198198
json["__type"] = BT::demangle(typeid(T));
199199
}
200200
};
201-
to_json_converters_.insert({ typeid(T), std::move(converter) });
202201
//---------------------------------------------
203-
// add the vector<T> converter
202+
// add the vector<T> converter (must be created before moving converter)
204203
auto vector_converter = [converter](const BT::Any& entry, nlohmann::json& json) {
205204
auto& vec = *const_cast<BT::Any&>(entry).castPtr<std::vector<T>>();
206205
for(const auto& item : vec)
@@ -210,6 +209,7 @@ inline void JsonExporter::addConverter(
210209
json.push_back(item_json);
211210
}
212211
};
212+
to_json_converters_.insert({ typeid(T), std::move(converter) });
213213
to_json_converters_.insert({ typeid(std::vector<T>), std::move(vector_converter) });
214214
}
215215

tests/gtest_ports.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,56 @@ TEST(PortTest, DefaultEmptyVector_Issue982)
778778
<< (result.empty() ? "" : result[0]) << "\"";
779779
}
780780

781+
// Issue #989: JsonExporter::addConverter(std::function) had a use-after-move
782+
// bug where `converter` was moved into to_json_converters_ before
783+
// `vector_converter` captured it, causing bad_function_call at runtime.
784+
struct TestPoint989
785+
{
786+
double x = 0;
787+
double y = 0;
788+
};
789+
790+
void TestPoint989ToJson(const TestPoint989& p, nlohmann::json& j)
791+
{
792+
j["x"] = p.x;
793+
j["y"] = p.y;
794+
}
795+
796+
void TestPoint989FromJson(const nlohmann::json& j, TestPoint989& p)
797+
{
798+
p.x = j.at("x").get<double>();
799+
p.y = j.at("y").get<double>();
800+
}
801+
802+
TEST(PortTest, JsonExporterVectorConverter_Issue989)
803+
{
804+
auto& exporter = JsonExporter::get();
805+
exporter.addConverter<TestPoint989>(&TestPoint989ToJson);
806+
exporter.addConverter<TestPoint989>(&TestPoint989FromJson);
807+
808+
// Single element conversion should work
809+
{
810+
BT::Any any_val(TestPoint989{ 1.0, 2.0 });
811+
nlohmann::json j;
812+
ASSERT_TRUE(exporter.toJson(any_val, j));
813+
EXPECT_DOUBLE_EQ(j["x"].get<double>(), 1.0);
814+
EXPECT_DOUBLE_EQ(j["y"].get<double>(), 2.0);
815+
}
816+
817+
// Vector conversion must not throw bad_function_call
818+
{
819+
std::vector<TestPoint989> vec = { { 1.0, 2.0 }, { 3.0, 4.0 } };
820+
BT::Any any_vec(vec);
821+
nlohmann::json j;
822+
ASSERT_NO_THROW(ASSERT_TRUE(exporter.toJson(any_vec, j)));
823+
ASSERT_EQ(j.size(), 2u);
824+
EXPECT_DOUBLE_EQ(j[0]["x"].get<double>(), 1.0);
825+
EXPECT_DOUBLE_EQ(j[0]["y"].get<double>(), 2.0);
826+
EXPECT_DOUBLE_EQ(j[1]["x"].get<double>(), 3.0);
827+
EXPECT_DOUBLE_EQ(j[1]["y"].get<double>(), 4.0);
828+
}
829+
}
830+
781831
// Issue #1065: passing a string literal like "1;2;3" through a SubTree port
782832
// to a LoopDouble node should work, but fails because the subtree remapping
783833
// stores the value as a plain std::string in the blackboard without converting

0 commit comments

Comments
 (0)