feat: add .members() iterator adapter for range-based for loops (#288) (#1679)
* feat: add .members() iterator adapter for range-based for loops (#288)
This adds a zero-allocation iterator adapter to `Json::Value` that enables idiomatic range-based for loops over object members. This allows iterating over key-value pairs without allocating a vector of keys via `getMemberNames()`, and cleanly supports C++17 structured bindings (e.g. `for (const auto& [name, val] : obj.members())`).
Fixes #288
* run ninja format
diff --git a/.github/workflows/abi-compatibility.yml b/.github/workflows/abi-compatibility.yml
index 881c4e8..1351c09 100644
--- a/.github/workflows/abi-compatibility.yml
+++ b/.github/workflows/abi-compatibility.yml
@@ -16,9 +16,17 @@
shared_libs: [ON, OFF]
include:
- jsoncpp_std: 11
+ app_std: 17
+ - jsoncpp_std: 17
+ app_std: 11
+ - jsoncpp_std: 11
app_std: 23
- jsoncpp_std: 23
app_std: 11
+ - jsoncpp_std: 17
+ app_std: 23
+ - jsoncpp_std: 23
+ app_std: 17
steps:
- name: checkout project
@@ -47,11 +55,12 @@
find_package(jsoncpp REQUIRED CONFIG)
- add_executable(abi_test stringView.cpp)
+ add_executable(abi_test jsontest.cpp fuzz.cpp main.cpp)
target_link_libraries(abi_test PRIVATE JsonCpp::JsonCpp)
EOF
- cp $GITHUB_WORKSPACE/example/stringView/stringView.cpp example-app/stringView.cpp
+ cp src/test_lib_json/*.cpp example-app/
+ cp src/test_lib_json/*.h example-app/
- name: build example app (C++${{ matrix.app_std }})
shell: bash
diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml
index 55452ac..2b2666d 100644
--- a/.github/workflows/cmake.yml
+++ b/.github/workflows/cmake.yml
@@ -12,6 +12,7 @@
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
+ cxx_standard: [11, 17]
steps:
- name: checkout project
@@ -19,4 +20,6 @@
- name: build project
uses: threeal/cmake-action@v2.0.0
+ with:
+ options: CMAKE_CXX_STANDARD=${{ matrix.cxx_standard }}
diff --git a/include/json/forwards.h b/include/json/forwards.h
index affe33a..2887bdd 100644
--- a/include/json/forwards.h
+++ b/include/json/forwards.h
@@ -37,6 +37,8 @@
class ValueIteratorBase;
class ValueIterator;
class ValueConstIterator;
+class ValueMembersView;
+class ValueConstMembersView;
} // namespace Json
diff --git a/include/json/value.h b/include/json/value.h
index 2007e6b..f14a71c 100644
--- a/include/json/value.h
+++ b/include/json/value.h
@@ -682,6 +682,11 @@
iterator begin();
iterator end();
+ // \brief Returns a view of member pairs for range-based for loops.
+ ValueMembersView members();
+ // \brief Returns a view of member pairs for range-based for loops.
+ ValueConstMembersView members() const;
+
/// \brief Returns a reference to the first element in the `Value`.
/// Requires that this value holds an array or json object, with at least one
/// element.
@@ -1040,6 +1045,131 @@
pointer operator->() const { return const_cast<pointer>(&deref()); }
};
+/** \brief Proxy struct to enable range-based for loops over object members.
+ */
+struct MemberProxy {
+ const String name;
+ Value& value;
+};
+
+/** \brief Proxy struct to enable range-based for loops over const object
+ * members.
+ */
+struct ConstMemberProxy {
+ const String name;
+ const Value& value;
+};
+
+/** \brief Iterator adapter for range-based for loops.
+ */
+class ValueMembersIterator {
+public:
+ using iterator_category = std::forward_iterator_tag;
+ using value_type = MemberProxy;
+ using difference_type = int;
+ using pointer = MemberProxy*;
+ using reference = MemberProxy;
+
+ ValueMembersIterator() = default;
+ explicit ValueMembersIterator(ValueIterator const& iter) : it_(iter) {}
+
+ ValueMembersIterator& operator++() {
+ ++it_;
+ return *this;
+ }
+ ValueMembersIterator operator++(int) {
+ ValueMembersIterator temp(*this);
+ ++*this;
+ return temp;
+ }
+ bool operator==(ValueMembersIterator const& other) const {
+ return it_ == other.it_;
+ }
+ bool operator!=(ValueMembersIterator const& other) const {
+ return it_ != other.it_;
+ }
+ MemberProxy operator*() const { return MemberProxy{it_.name(), *it_}; }
+
+private:
+ ValueIterator it_;
+};
+
+/** \brief Iterator adapter for range-based for loops.
+ */
+class ValueConstMembersIterator {
+public:
+ using iterator_category = std::forward_iterator_tag;
+ using value_type = ConstMemberProxy;
+ using difference_type = int;
+ using pointer = ConstMemberProxy*;
+ using reference = ConstMemberProxy;
+
+ ValueConstMembersIterator() = default;
+ explicit ValueConstMembersIterator(ValueConstIterator const& iter)
+ : it_(iter) {}
+
+ ValueConstMembersIterator& operator++() {
+ ++it_;
+ return *this;
+ }
+ ValueConstMembersIterator operator++(int) {
+ ValueConstMembersIterator temp(*this);
+ ++*this;
+ return temp;
+ }
+ bool operator==(ValueConstMembersIterator const& other) const {
+ return it_ == other.it_;
+ }
+ bool operator!=(ValueConstMembersIterator const& other) const {
+ return it_ != other.it_;
+ }
+ ConstMemberProxy operator*() const {
+ return ConstMemberProxy{it_.name(), *it_};
+ }
+
+private:
+ ValueConstIterator it_;
+};
+
+/** \brief Range-based for loop adapter for object members.
+ */
+class ValueMembersView {
+public:
+ ValueMembersView(ValueIterator begin, ValueIterator end)
+ : begin_(begin), end_(end) {}
+ ValueMembersIterator begin() const { return ValueMembersIterator(begin_); }
+ ValueMembersIterator end() const { return ValueMembersIterator(end_); }
+
+private:
+ ValueIterator begin_;
+ ValueIterator end_;
+};
+
+/** \brief Range-based for loop adapter for object members.
+ */
+class ValueConstMembersView {
+public:
+ ValueConstMembersView(ValueConstIterator begin, ValueConstIterator end)
+ : begin_(begin), end_(end) {}
+ ValueConstMembersIterator begin() const {
+ return ValueConstMembersIterator(begin_);
+ }
+ ValueConstMembersIterator end() const {
+ return ValueConstMembersIterator(end_);
+ }
+
+private:
+ ValueConstIterator begin_;
+ ValueConstIterator end_;
+};
+
+inline ValueMembersView Value::members() {
+ return ValueMembersView(begin(), end());
+}
+inline ValueConstMembersView Value::members() const {
+ return ValueConstMembersView(begin(), end());
+}
+
inline void swap(Value& a, Value& b) { a.swap(b); }
inline const Value& Value::front() const { return *begin(); }
diff --git a/src/test_lib_json/main.cpp b/src/test_lib_json/main.cpp
index 4c000fa..501aba1 100644
--- a/src/test_lib_json/main.cpp
+++ b/src/test_lib_json/main.cpp
@@ -3924,6 +3924,54 @@
struct IteratorTest : JsonTest::TestCase {};
+JSONTEST_FIXTURE_LOCAL(IteratorTest, members) {
+ Json::Value j;
+ j["k1"] = "a";
+ j["k2"] = "b";
+
+ std::vector<std::string> keys;
+ std::vector<std::string> values;
+
+ for (const auto& member : j.members()) {
+ keys.push_back(member.name);
+ values.push_back(member.value.asString());
+ }
+
+ JSONTEST_ASSERT((keys == std::vector<std::string>{"k1", "k2"}));
+ JSONTEST_ASSERT((values == std::vector<std::string>{"a", "b"}));
+
+ // Test modification through value reference
+ for (const auto& member : j.members()) {
+ member.value = "c";
+ }
+
+ JSONTEST_ASSERT(j["k1"].asString() == "c");
+
+ // Test const members
+ const Json::Value& cj = j;
+ keys.clear();
+ values.clear();
+
+ for (const auto& member : cj.members()) {
+ keys.push_back(member.name);
+ values.push_back(member.value.asString());
+ }
+
+ JSONTEST_ASSERT((keys == std::vector<std::string>{"k1", "k2"}));
+ JSONTEST_ASSERT((values == std::vector<std::string>{"c", "c"}));
+
+#if __cplusplus >= 201703L
+ keys.clear();
+ values.clear();
+ for (auto const& [k, v] : cj.members()) {
+ keys.push_back(k);
+ values.push_back(v.asString());
+ }
+ JSONTEST_ASSERT((keys == std::vector<std::string>{"k1", "k2"}));
+ JSONTEST_ASSERT((values == std::vector<std::string>{"c", "c"}));
+#endif
+}
+
JSONTEST_FIXTURE_LOCAL(IteratorTest, convert) {
Json::Value j;
const Json::Value& cj = j;