Compare commits

..
Author SHA1 Message Date
Niels Lohmann 33ef25099d Keep external headers as #include when amalgamating
A header that builds on json.hpp (such as the planned json_view.hpp) must
not inline json.hpp: its single-header version would contain a second copy
of the library, and that copy would change with every library change.

The optional config key "external" lists include paths that are kept as
#include directives. Only the first directive per path is kept; repeated
ones are commented out, as the tool already does for inlined headers.
config_json_view.json uses it for json_view.hpp; the existing configs do
not set it, and json.hpp and json_fwd.hpp regenerate byte-identically.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 03:49:24 +02:00
10 changed files with 50 additions and 555 deletions
-11
View File
@@ -28,11 +28,6 @@ A minimal map-like container that preserves insertion order for use within [`nlo
The type uses a `std::vector` to store object elements. Therefore, adding elements can yield a reallocation in which
case all iterators (including the `end()` iterator) and all references to the elements are invalidated.
When the storage grows, the keys are copied and the mapped values are moved to the new storage. A plain `std::vector`
would copy the whole elements instead, because their `#!cpp const` keys make them not nothrow move constructible; for
[`ordered_json`](ordered_json.md), this would be a deep copy of every nested value. The values are only copied if
`T` is not default constructible or not nothrow move assignable.
## Member types
- **key_type** - key type (`Key`)
@@ -61,11 +56,6 @@ std::equal_to<> // since C++14
- **find**
- **insert**
## Exception safety
**emplace**, **operator\[\]**, and **insert(value)** have the strong exception guarantee: if an exception is thrown (for
instance, because copying a key or allocating memory fails), the contents of the container are unchanged.
## Complexity
Because the elements are stored in a `std::vector` in insertion order, there is no index to look a key up by. Every
@@ -132,4 +122,3 @@ This differs from `#!cpp std::map`, where the same operations are O(log n).
- Added in version 3.9.0 to implement [`nlohmann::ordered_json`](ordered_json.md).
- Added **key_compare** member in version 3.11.0.
- Changed in version 3.13.0: growing the storage moves the mapped values instead of copying them.
+5 -65
View File
@@ -8,15 +8,13 @@
#pragma once
#include <algorithm> // max, min
#include <functional> // equal_to, less
#include <initializer_list> // initializer_list
#include <iterator> // input_iterator_tag, iterator_traits
#include <memory> // allocator
#include <stdexcept> // for out_of_range
#include <tuple> // forward_as_tuple
#include <type_traits> // enable_if, integral_constant, is_convertible, is_nothrow_move_constructible
#include <utility> // forward, move, pair, piecewise_construct
#include <type_traits> // enable_if, is_convertible
#include <utility> // pair
#include <vector> // vector
#include <nlohmann/detail/macro_scope.hpp>
@@ -81,7 +79,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(key, std::forward<T>(t));
Container::emplace_back(key, std::forward<T>(t));
return {std::prev(this->end()), true};
}
@@ -96,7 +94,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(std::forward<KeyType>(key), std::forward<T>(t));
Container::emplace_back(std::forward<KeyType>(key), std::forward<T>(t));
return {std::prev(this->end()), true};
}
@@ -370,7 +368,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(value);
Container::push_back(value);
return {--this->end(), true};
}
@@ -388,64 +386,6 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
}
private:
/*!
@brief add an element whose key is not yet contained at the end
A std::vector copies all elements when it grows, because their const keys
make them not nothrow move constructible. For ordered_json, this is a deep
copy of every value. Where the strong exception guarantee can be kept, grow
the storage here instead, copying only the keys and moving the values.
*/
template<typename... Args>
void append(Args&& ... args)
{
// evaluated here rather than at class scope, because T is still
// incomplete when basic_json instantiates its object_t
using move_values = std::integral_constant<bool, detail::conjunction<
detail::negation<std::is_nothrow_move_constructible<value_type>>,
std::is_copy_constructible<key_type>,
detail::is_default_constructible<mapped_type>,
std::is_nothrow_move_assignable<mapped_type>>::value>;
append_impl(move_values{}, std::forward<Args>(args)...);
}
template<typename... Args>
void append_impl(std::true_type /*unused*/, Args&& ... args)
{
if (this->size() < this->capacity())
{
Container::emplace_back(std::forward<Args>(args)...);
return;
}
// 1. May throw, but only changes tmp: copy the keys, value-initialize
// the values, and add the new element. The arguments may refer to
// elements of this container, so they are used before any value is
// moved out of it.
Container tmp(this->get_allocator()); // equal allocators, so swap() is valid
tmp.reserve((std::min)(this->max_size(), (std::max)(size_type{1}, 2 * this->size())));
for (const auto& element : *this)
{
tmp.emplace_back(std::piecewise_construct, std::forward_as_tuple(element.first), std::forward_as_tuple());
}
tmp.emplace_back(std::forward<Args>(args)...);
// 2. Cannot throw: move the values over and adopt the new storage.
auto it = tmp.begin();
for (auto& element : *this)
{
it->second = std::move(element.second);
++it;
}
Container::swap(tmp);
}
template<typename... Args>
void append_impl(std::false_type /*unused*/, Args&& ... args)
{
Container::emplace_back(std::forward<Args>(args)...);
}
JSON_NO_UNIQUE_ADDRESS key_compare m_compare = key_compare();
};
+5 -65
View File
@@ -25768,15 +25768,13 @@ NLOHMANN_JSON_NAMESPACE_END
#include <algorithm> // max, min
#include <functional> // equal_to, less
#include <initializer_list> // initializer_list
#include <iterator> // input_iterator_tag, iterator_traits
#include <memory> // allocator
#include <stdexcept> // for out_of_range
#include <tuple> // forward_as_tuple
#include <type_traits> // enable_if, integral_constant, is_convertible, is_nothrow_move_constructible
#include <utility> // forward, move, pair, piecewise_construct
#include <type_traits> // enable_if, is_convertible
#include <utility> // pair
#include <vector> // vector
// #include <nlohmann/detail/macro_scope.hpp>
@@ -25843,7 +25841,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(key, std::forward<T>(t));
Container::emplace_back(key, std::forward<T>(t));
return {std::prev(this->end()), true};
}
@@ -25858,7 +25856,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(std::forward<KeyType>(key), std::forward<T>(t));
Container::emplace_back(std::forward<KeyType>(key), std::forward<T>(t));
return {std::prev(this->end()), true};
}
@@ -26132,7 +26130,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
append(value);
Container::push_back(value);
return {--this->end(), true};
}
@@ -26150,64 +26148,6 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
}
private:
/*!
@brief add an element whose key is not yet contained at the end
A std::vector copies all elements when it grows, because their const keys
make them not nothrow move constructible. For ordered_json, this is a deep
copy of every value. Where the strong exception guarantee can be kept, grow
the storage here instead, copying only the keys and moving the values.
*/
template<typename... Args>
void append(Args&& ... args)
{
// evaluated here rather than at class scope, because T is still
// incomplete when basic_json instantiates its object_t
using move_values = std::integral_constant<bool, detail::conjunction<
detail::negation<std::is_nothrow_move_constructible<value_type>>,
std::is_copy_constructible<key_type>,
detail::is_default_constructible<mapped_type>,
std::is_nothrow_move_assignable<mapped_type>>::value>;
append_impl(move_values{}, std::forward<Args>(args)...);
}
template<typename... Args>
void append_impl(std::true_type /*unused*/, Args&& ... args)
{
if (this->size() < this->capacity())
{
Container::emplace_back(std::forward<Args>(args)...);
return;
}
// 1. May throw, but only changes tmp: copy the keys, value-initialize
// the values, and add the new element. The arguments may refer to
// elements of this container, so they are used before any value is
// moved out of it.
Container tmp(this->get_allocator()); // equal allocators, so swap() is valid
tmp.reserve((std::min)(this->max_size(), (std::max)(size_type{1}, 2 * this->size())));
for (const auto& element : *this)
{
tmp.emplace_back(std::piecewise_construct, std::forward_as_tuple(element.first), std::forward_as_tuple());
}
tmp.emplace_back(std::forward<Args>(args)...);
// 2. Cannot throw: move the values over and adopt the new storage.
auto it = tmp.begin();
for (auto& element : *this)
{
it->second = std::move(element.second);
++it;
}
Container::swap(tmp);
}
template<typename... Args>
void append_impl(std::false_type /*unused*/, Args&& ... args)
{
Container::emplace_back(std::forward<Args>(args)...);
}
JSON_NO_UNIQUE_ADDRESS key_compare m_compare = key_compare();
};
-33
View File
@@ -119,39 +119,6 @@ BENCHMARK_CAPTURE(ParseIndented, canada / 4, TEST_DATA_DIRECTORY "/nativej
BENCHMARK_CAPTURE(ParseIndented, citm_catalog / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", 4);
BENCHMARK_CAPTURE(ParseIndented, twitter / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", 4);
//////////////////////////////////////////////////////////////////////////////
// parse JSON from string into an ordered_json
//
// Same as ParseString above, but with nlohmann::ordered_json, whose objects
// keep their members in a vector: the pair of rows shows what preserving the
// insertion order costs.
//////////////////////////////////////////////////////////////////////////////
static void ParseStringOrdered(benchmark::State& state, const char* filename)
{
std::ifstream f(filename);
std::string str((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
while (state.KeepRunning())
{
state.PauseTiming();
auto* j = new nlohmann::ordered_json();
state.ResumeTiming();
*j = nlohmann::ordered_json::parse(str);
state.PauseTiming();
delete j;
state.ResumeTiming();
}
state.SetBytesProcessed(state.iterations() * str.size());
}
BENCHMARK_CAPTURE(ParseStringOrdered, jeopardy, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json");
BENCHMARK_CAPTURE(ParseStringOrdered, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
BENCHMARK_CAPTURE(ParseStringOrdered, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
BENCHMARK_CAPTURE(ParseStringOrdered, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
//////////////////////////////////////////////////////////////////////////////
// serialize JSON
//////////////////////////////////////////////////////////////////////////////
-18
View File
@@ -46,24 +46,6 @@ TEST_CASE("Tests with disabled exceptions")
CHECK(*sax_no_exception::error_string == "[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'x'");
delete sax_no_exception::error_string; // NOLINT(cppcoreguidelines-owning-memory)
}
SECTION("growing an ordered_json object")
{
auto j = nlohmann::ordered_json::object();
for (int i = 0; i < 100; ++i)
{
j[std::to_string(i)] = {{"nested", i}};
}
CHECK(j.size() == 100);
int i = 0;
for (const auto& element : j.items())
{
CHECK(element.key() == std::to_string(i));
CHECK(element.value()["nested"] == i);
++i;
}
}
}
DOCTEST_GCC_SUPPRESS_WARNING_POP
-358
View File
@@ -11,97 +11,6 @@
#include <nlohmann/json.hpp>
using nlohmann::ordered_map;
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
// The EDG front end (Intel icpc, NVIDIA nvc++) considers the defaulted move
// constructor of std::pair<const Key, T> noexcept even if copying Key can
// throw. std::vector then moves such elements itself when it grows (and calls
// std::terminate if a key copy throws), so ordered_map leaves growing to it.
#if defined(__EDG__)
#define JSON_TEST_PAIR_MOVE_IS_NOEXCEPT
#endif
namespace
{
// number of copies made of counted values
int value_copies = 0;
// a mapped type that counts its copies; moving from it leaves -1 behind
struct counted // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions)
{
int payload = 0;
counted() = default;
explicit counted(int p) noexcept : payload(p) {}
counted(const counted& other) : payload(other.payload)
{
++value_copies;
}
counted(counted&& other) noexcept : payload(other.payload)
{
other.payload = -1;
}
counted& operator=(const counted&) = delete;
counted& operator=(counted&& other) noexcept
{
payload = other.payload;
other.payload = -1;
return *this;
}
};
#if !defined(JSON_NOEXCEPTION) && !defined(JSON_TEST_PAIR_MOVE_IS_NOEXCEPT)
// number of throwing_key copies that still succeed; the next one throws
// (a negative value means that copies never throw)
int key_copies_until_throw = -1;
// a key type whose copy constructor can be made to throw
struct throwing_key // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions)
{
int id = 0;
explicit throwing_key(int i) noexcept : id(i) {}
throwing_key(const throwing_key& other) : id(other.id)
{
if (key_copies_until_throw == 0)
{
throw std::runtime_error("key copy failed");
}
if (key_copies_until_throw > 0)
{
--key_copies_until_throw;
}
}
throwing_key& operator=(const throwing_key&) = delete;
friend bool operator==(const throwing_key& lhs, const throwing_key& rhs) noexcept
{
return lhs.id == rhs.id;
}
};
#endif
// a mapped type that cannot be default-constructed
struct no_default
{
explicit no_default(int v) noexcept : value(v) {}
int value;
};
// ordered_json must keep moving its values when an object grows
using ordered_object_t = nlohmann::ordered_json::object_t;
#if !defined(JSON_TEST_PAIR_MOVE_IS_NOEXCEPT)
static_assert(!std::is_nothrow_move_constructible<ordered_object_t::value_type>::value, "std::vector would move the elements itself");
#endif
static_assert(std::is_copy_constructible<ordered_object_t::key_type>::value, "keys must be copyable");
static_assert(std::is_default_constructible<ordered_object_t::mapped_type>::value, "values must be default-constructible");
static_assert(std::is_nothrow_move_assignable<ordered_object_t::mapped_type>::value, "values must be nothrow move-assignable");
} // namespace
TEST_CASE("ordered_map")
{
SECTION("constructor")
@@ -404,270 +313,3 @@ TEST_CASE("ordered_map")
}
}
}
TEST_CASE("ordered_map growth")
{
SECTION("values are moved, not copied, when the storage grows")
{
ordered_map<std::string, counted> om;
std::size_t growths = 0;
value_copies = 0;
// inserts 100 elements with the given function and counts the growths
const auto fill = [&om, &growths](void (*insert)(ordered_map<std::string, counted>&, int))
{
for (int i = 0; i < 100; ++i)
{
const auto old_capacity = om.capacity();
insert(om, i);
if (om.capacity() > old_capacity)
{
++growths;
}
}
};
// checks that the elements are in insertion order with their values
const auto check_contents = [&om]
{
CHECK(om.size() == 100);
int i = 0;
for (const auto& element : om)
{
CHECK(element.first == std::to_string(i));
CHECK(element.second.payload == i);
++i;
}
};
SECTION("emplace")
{
fill([](ordered_map<std::string, counted>& m, int i)
{
m.emplace(std::to_string(i), counted(i));
});
CHECK(growths >= 3);
CHECK(value_copies == 0);
check_contents();
}
SECTION("operator[]")
{
fill([](ordered_map<std::string, counted>& m, int i)
{
m[std::to_string(i)] = counted(i);
});
CHECK(growths >= 3);
CHECK(value_copies == 0);
check_contents();
}
SECTION("insert(value_type&&)")
{
fill([](ordered_map<std::string, counted>& m, int i)
{
m.insert({std::to_string(i), counted(i)});
});
CHECK(growths >= 3);
CHECK(value_copies == 0);
check_contents();
}
SECTION("insert(const value_type&)")
{
fill([](ordered_map<std::string, counted>& m, int i)
{
const std::pair<const std::string, counted> value(std::to_string(i), counted(i));
m.insert(value);
});
CHECK(growths >= 3);
// only the inserted values are copied
CHECK(value_copies == 100);
check_contents();
}
SECTION("insert(first, last)")
{
std::vector<std::pair<const std::string, counted>> values;
values.reserve(100);
for (int i = 0; i < 100; ++i)
{
values.emplace_back(std::to_string(i), counted(i));
}
value_copies = 0;
om.insert(values.cbegin(), values.cend());
// only the inserted values are copied
CHECK(value_copies == 100);
check_contents();
}
}
SECTION("elements keep their order and values over many growths")
{
ordered_map<std::string, counted> om;
for (int i = 0; i < 1000; ++i)
{
om.emplace(std::to_string(i), counted(i));
}
CHECK(om.size() == 1000);
int i = 0;
for (const auto& element : om)
{
CHECK(element.first == std::to_string(i));
CHECK(element.second.payload == i);
++i;
}
}
SECTION("arguments may refer to elements of the full container")
{
SECTION("moving a value out of the container")
{
ordered_map<std::string, counted> om;
om.reserve(4);
while (om.size() < om.capacity())
{
const auto i = static_cast<int>(om.size());
om.emplace(std::to_string(i), counted(i));
}
const auto size = om.size();
om.emplace("new", std::move(om.at("0")));
CHECK(om.size() == size + 1);
CHECK(om.at("new").payload == 0);
CHECK(om.at("0").payload == -1);
}
SECTION("using a value as key")
{
ordered_map<std::string, std::string> om;
om.reserve(4);
while (om.size() < om.capacity())
{
const auto i = std::to_string(om.size());
om.emplace("k" + i, "v" + i);
}
const auto size = om.size();
om.emplace(om.at("k0"), std::string("x"));
CHECK(om.size() == size + 1);
CHECK(om.at("k0") == "v0");
CHECK(om.at("v0") == "x");
}
SECTION("ordered_json")
{
auto j = nlohmann::ordered_json::object();
auto& object = j.get_ref<nlohmann::ordered_json::object_t&>();
object.reserve(4);
while (object.size() < object.capacity())
{
const auto i = std::to_string(object.size());
j[i] = "a value that is too long for the small string optimization " + i;
}
const auto size = j.size();
j.emplace("new", std::move(j["0"]));
CHECK(j.size() == size + 1);
CHECK(j["new"] == "a value that is too long for the small string optimization 0");
CHECK(j["0"].is_null());
}
}
#if !defined(JSON_NOEXCEPTION) && !defined(JSON_TEST_PAIR_MOVE_IS_NOEXCEPT)
SECTION("the container is unchanged if growing it throws")
{
ordered_map<throwing_key, counted> om;
om.reserve(4);
while (om.size() < om.capacity())
{
const auto i = static_cast<int>(om.size());
om.emplace(throwing_key(i), counted(i));
}
const auto size = om.size();
const auto capacity = om.capacity();
// checks that the elements are unchanged
const auto check_unchanged = [&om, size, capacity]
{
CHECK(om.size() == size);
CHECK(om.capacity() == capacity);
int i = 0;
for (const auto& element : om)
{
CHECK(element.first.id == i);
CHECK(element.second.payload == i);
++i;
}
};
SECTION("emplace")
{
// growing copies the existing keys and then the new one; let each of these copies throw
for (std::size_t k = 0; k <= size; ++k)
{
counted value(100);
key_copies_until_throw = static_cast<int>(k);
CHECK_THROWS_AS(om.emplace(throwing_key(100), std::move(value)), std::runtime_error);
key_copies_until_throw = -1;
check_unchanged();
CHECK(value.payload == 100); // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved)
}
om.emplace(throwing_key(100), counted(100));
CHECK(om.size() == size + 1);
CHECK(om.capacity() > capacity);
CHECK(om.at(throwing_key(100)).payload == 100);
}
SECTION("insert(const value_type&)")
{
const std::pair<const throwing_key, counted> value(throwing_key(100), counted(100));
value_copies = 0;
key_copies_until_throw = static_cast<int>(size / 2);
CHECK_THROWS_AS(om.insert(value), std::runtime_error);
key_copies_until_throw = -1;
check_unchanged();
CHECK(value_copies == 0);
}
}
#endif
SECTION("elements that std::vector moves, or that cannot be moved back")
{
SECTION("nothrow move-constructible elements")
{
ordered_map<int, counted> om;
value_copies = 0;
for (int i = 0; i < 100; ++i)
{
om.emplace(i, counted(i));
}
CHECK(om.size() == 100);
CHECK(value_copies == 0);
}
SECTION("mapped type without default constructor")
{
ordered_map<std::string, no_default> om;
for (int i = 0; i < 100; ++i)
{
om.emplace(std::to_string(i), no_default(i));
}
CHECK(om.size() == 100);
int i = 0;
for (const auto& element : om)
{
CHECK(element.first == std::to_string(i));
CHECK(element.second.value == i);
++i;
}
}
}
}
+3
View File
@@ -8,3 +8,6 @@ The following changes have been made to the code with respect to <https://github
- membership check
- made function from `_is_within`
- removed unused variable `actual_path`
- Added the optional config key `external`: include paths listed there are kept as
`#include` directives instead of being inlined (the first directive per path; the
repeated ones are commented out).
+5
View File
@@ -57,6 +57,11 @@ Python v.2.7.0 or higher is required.
amalgamation. Have a look at `test/source.c.json` and `test/include.h.json`
to see two examples.
The optional `external` list names include paths that are kept as `#include`
directives instead of being inlined, e.g. `["nlohmann/json.hpp"]` for a header
that includes another amalgamated header. Only the first directive for each
of these paths is kept; the repeated ones are commented out.
* The `-s, --source` option should specify the path to the source directory.
This is useful for supporting separate source and build directories.
+23 -5
View File
@@ -62,6 +62,10 @@ class Amalgamation(object):
return None
def __init__(self, args):
# include paths that are kept as #include directives instead of
# being inlined (e.g. a header amalgamated on its own)
self.external = []
self.included_external = []
with open(args.config, 'r') as f:
config = json.loads(f.read())
for key in config:
@@ -220,11 +224,14 @@ class TranslationUnit(object):
while include_match:
if not _is_within(include_match, skippable_contexts):
include_path = include_match.group("path")
search_same_dir = include_match.group(1) == '"'
found_included_path = self.amalgamation.find_included_file(
include_path, self.file_dir if search_same_dir else None)
if found_included_path:
includes.append((include_match, found_included_path))
if include_path in self.amalgamation.external:
includes.append((include_match, None))
else:
search_same_dir = include_match.group(1) == '"'
found_included_path = self.amalgamation.find_included_file(
include_path, self.file_dir if search_same_dir else None)
if found_included_path:
includes.append((include_match, found_included_path))
include_match = self.include_pattern.search(self.content,
include_match.end())
@@ -235,6 +242,17 @@ class TranslationUnit(object):
for include in includes:
include_match, found_included_path = include
tmp_content += self.content[prev_end:include_match.start()]
if found_included_path is None:
# an external header: keep the first directive and comment
# out the repeated ones
include_path = include_match.group("path")
if include_path in self.amalgamation.included_external:
tmp_content += "// {0}".format(include_match.group(0))
else:
self.amalgamation.included_external.append(include_path)
tmp_content += include_match.group(0)
prev_end = include_match.end()
continue
tmp_content += "// {0}\n".format(include_match.group(0))
if found_included_path not in self.amalgamation.included_files:
t = TranslationUnit(found_included_path, self.amalgamation, False)
+9
View File
@@ -0,0 +1,9 @@
{
"project": "JSON for Modern C++",
"target": "single_include/nlohmann/json_view.hpp",
"sources": [
"include/nlohmann/json_view.hpp"
],
"include_paths": ["include"],
"external": ["nlohmann/json.hpp"]
}