Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion include/cpp_yyjson.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1666,7 +1666,12 @@ namespace yyjson
auto& get_has_parent() & noexcept
{
if constexpr (is_value_type)
{
// moved-from 对象的 has_parent_(shared_ptr)可能被移空;
// 防御性重建,避免解引用空 shared_ptr 导致 null-deref。
if (!has_parent_) has_parent_ = std::make_shared<bool>(false);
return *has_parent_;
}
else
return has_parent_;
}
Expand Down Expand Up @@ -2014,7 +2019,20 @@ namespace yyjson
}
mutable_value_base& operator=(mutable_value_base&& t) noexcept
{
base::doc_.set_value(base::val_, std::move(t), base::get_has_parent());
if constexpr (base::is_value_type)
{
// owning value 的 move = 整棵 doc 所有权转移。
// set_value 的"内容覆盖 + 挂 children 保活"是 reference value 的 node
// 编辑语义,owning 走它会让 moved-from val_ 悬挂、has_parent_ 空,且
// 每次 move 把 src doc 塞 dst.children 累积。reference value 仍走 set_value。
base::doc_ = std::move(t.doc_);
base::val_ = t.val_;
base::has_parent_ = std::move(t.has_parent_);
}
else
{
base::doc_.set_value(base::val_, std::move(t), base::get_has_parent());
}
return *this;
}
template <create_value_callable T>
Expand Down
5 changes: 5 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ target_link_libraries("${PROJECT_NAME}_test_value_cast"
PRIVATE ${PROJECT_NAME} GTest::gtest GTest::gtest_main)
gtest_discover_tests("${PROJECT_NAME}_test_value_cast" DISCOVERY_MODE PRE_TEST)

add_executable("${PROJECT_NAME}_test_owning_value_move" test_owning_value_move.cpp)
target_link_libraries("${PROJECT_NAME}_test_owning_value_move"
PRIVATE ${PROJECT_NAME} GTest::gtest GTest::gtest_main)
gtest_discover_tests("${PROJECT_NAME}_test_owning_value_move" DISCOVERY_MODE PRE_TEST)


# benchmark
if(CPPYYJSON_BUILD_BENCH)
Expand Down
8 changes: 4 additions & 4 deletions test/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -652,16 +652,16 @@ TEST(Writer, ValueConversion)
v = value();
EXPECT_TRUE(v.is_null());
EXPECT_EQ(nullptr, *v.as_null());
EXPECT_EQ(1, v.count_children());
EXPECT_EQ(0, v.count_children());
v = {1, value(2), value(3)};
EXPECT_TRUE(v.is_array());
EXPECT_EQ(4, v.count_children());
EXPECT_EQ(3, v.count_children());
v = {{"a", 1}, {"b", value(2)}, {"c", value(3)}};
EXPECT_TRUE(v.is_object());
EXPECT_EQ(7, v.count_children());
EXPECT_EQ(6, v.count_children());
v = {{{"a", 1}, {"b", value(2)}, {"c", value(3)}}, yyjson::copy_string};
EXPECT_TRUE(v.is_object());
EXPECT_EQ(8, v.count_children());
EXPECT_EQ(3, v.count_children());

// method return types
static_assert(std::same_as<decltype(std::declval<value&>().as_int()), std::optional<std::int64_t>>);
Expand Down
76 changes: 76 additions & 0 deletions test/test_owning_value_move.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Regression: owning yyjson::value (mutable_value_base<mutable_document>) must
// be movable/assignable so it can live in standard containers and be reordered
// by std::sort / std::swap.
//
// Before the fix, operator=(value&&) unconditionally used set_value (node
// content overwrite + cross-doc keepalive), which:
// - accumulated the source's whole document into dst.doc_.children on every
// move-assign (memory bloat under std::sort's repeated shuffling);
// - dereferenced this->has_parent_ (a shared_ptr<bool>) that move-construction
// leaves empty on the moved-from object -> null-pointer dereference when
// std::sort's insertion sort move-assigns into a moved-from slot.
//
// The fix routes owning move-assign through document ownership transfer
// (doc_/val_/has_parent_) and makes get_has_parent() defensively rehydrate an
// empty shared_ptr. These tests pin both the contract and the crash scenario.
#include <algorithm>
#include <cassert>
#include <cpp_yyjson.hpp>
#include <gtest/gtest.h>
#include <type_traits>
#include <utility>
#include <vector>

// 编译期契约:owning value 必须可移动构造/赋值(nothrow)。用 is_*_v trait 而非
// concept(MSVC 对复杂模板的 concept/SFINAE 检查不可靠,见 test.cpp 在 MSVC 跳过)。
static_assert(std::is_move_constructible_v<yyjson::value>);
static_assert(std::is_move_assignable_v<yyjson::value>);
static_assert(std::is_nothrow_move_constructible_v<yyjson::value>);
static_assert(std::is_nothrow_move_assignable_v<yyjson::value>);

namespace
{
yyjson::value make_int_value(int i)
{
yyjson::value v;
v = i;
return v;
}
}

// 缺陷①:moved-from 对象被重新 move-assign 不应 null-deref。
TEST(OwningValueMove, MovedFromCanBeReassigned)
{
auto a = make_int_value(1);
auto b = make_int_value(2);
auto tmp = std::move(a); // a 变 moved-from(has_parent_ 被移空)
a = std::move(b); // 修复前:operator=(value&&) 解引用空 has_parent_ -> 💥
EXPECT_EQ(a.as_sint().value_or(-1), 2);
(void)tmp;
}

// 缺陷②:vector<value> + std::sort 不崩且结果正确。
TEST(OwningValueMove, VectorSortDoesNotCrash)
{
std::vector<yyjson::value> v;
for (int i = 3; i >= 0; --i) // 逆序,确保 sort 触发 insertion 搬运
{
v.push_back(make_int_value(i));
}
std::sort(v.begin(), v.end(), [](const yyjson::value& x, const yyjson::value& y) {
return x.as_sint().value_or(0) < y.as_sint().value_or(0);
});
ASSERT_EQ(v.size(), 4u);
for (int i = 0; i < 4; ++i)
{
EXPECT_EQ(v[i].as_sint().value_or(-1), i) << "index " << i;
}
}

// 基本 move 语义:内容随所有权转移。
TEST(OwningValueMove, MoveTransfersContent)
{
auto a = make_int_value(42);
yyjson::value b = std::move(a);
EXPECT_EQ(b.as_sint().value_or(-1), 42);
}
Loading