Files
json/tests/src/unit-allocator.cpp
T
Niels Lohmann 993e37e7ec Reduce test compile times (spike)
Two changes roughly halve the CPU time needed to build the unit tests
(clang 21: ~300 s -> ~150 s, GCC 16: ~750-820 s -> ~395-420 s, Debug):

- Move the C++14/17/20-dependent tests into separate
  unit-<name>-cpp<N>.cpp files. A test file is built for every standard
  whose JSON_HAS_CPP_<N> macro it mentions, so far whole large files were
  rebuilt for C++14/17/20 because of a few #ifdef sections. The main files
  are now built for C++11 only; the ci_test_*_cxx<N> jobs still build
  every file for every standard.

- Add the CMake option JSON_TestUnityBuild (ON by default, OFF with
  MinGW): compatible test files are compiled in batches as one
  translation unit so they share the template instantiations of the
  library. Each file keeps its own CTest test, which runs the batch
  executable filtered to that file's test cases. The binary-format tests
  form an explicit group; the rest is batched by JSON_TestUnityBatchSize.

Fix the name clashes that merging files exposed, document the rules for
test files in tests/README.md, and point CONTRIBUTING.md to it.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-09 16:49:45 +02:00

788 lines
26 KiB
C++

// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#define JSON_TESTS_PRIVATE
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <valarray>
namespace
{
// special test case to check if memory is leaked if constructor throws
template<class T>
struct bad_allocator : std::allocator<T>
{
using std::allocator<T>::allocator;
bad_allocator() = default;
template<class U> bad_allocator(const bad_allocator<U>& /*unused*/) { }
template<class... Args>
[[noreturn]] void construct(T* /*unused*/, Args&& ... /*unused*/) // NOLINT(cppcoreguidelines-missing-std-forward)
{
throw std::bad_alloc();
}
template <class U>
struct rebind
{
using other = bad_allocator<U>;
};
};
} // namespace
TEST_CASE("get_allocator")
{
const auto alloc = nlohmann::json::get_allocator();
CHECK(alloc == std::allocator<nlohmann::json>());
}
TEST_CASE("bad_alloc")
{
SECTION("bad_alloc")
{
// create JSON type using the throwing allocator
using bad_json = nlohmann::json::with_allocator_t<bad_allocator>;
// creating an object should throw
CHECK_THROWS_AS(bad_json(bad_json::value_t::object), std::bad_alloc&);
}
}
namespace
{
bool next_construct_fails = false;
bool next_destroy_fails = false;
bool next_deallocate_fails = false;
template<class T>
struct my_allocator : std::allocator<T>
{
using std::allocator<T>::allocator;
template<class... Args>
void construct(T* p, Args&& ... args)
{
if (next_construct_fails)
{
next_construct_fails = false;
throw std::bad_alloc();
}
::new (reinterpret_cast<void*>(p)) T(std::forward<Args>(args)...);
}
void deallocate(T* p, std::size_t n)
{
if (next_deallocate_fails)
{
next_deallocate_fails = false;
throw std::bad_alloc();
}
std::allocator<T>::deallocate(p, n);
}
void destroy(T* p)
{
if (next_destroy_fails)
{
next_destroy_fails = false;
throw std::bad_alloc();
}
static_cast<void>(p); // fix MSVC's C4100 warning
p->~T();
}
template <class U>
struct rebind
{
using other = my_allocator<U>;
};
};
// allows deletion of raw pointer, usually hold by json_value
template<class T>
void my_allocator_clean_up(T* p)
{
assert(p != nullptr);
my_allocator<T> alloc;
alloc.destroy(p);
alloc.deallocate(p, 1);
}
} // namespace
TEST_CASE("controlled bad_alloc")
{
// create JSON type using the throwing allocator
using my_json = nlohmann::json::with_allocator_t<my_allocator>;
SECTION("class json_value")
{
SECTION("json_value(value_t)")
{
SECTION("object")
{
next_construct_fails = false;
auto t = my_json::value_t::object;
CHECK_NOTHROW(my_allocator_clean_up(my_json::json_value(t).object));
next_construct_fails = true;
CHECK_THROWS_AS(my_json::json_value(t), std::bad_alloc&);
next_construct_fails = false;
}
SECTION("array")
{
next_construct_fails = false;
auto t = my_json::value_t::array;
CHECK_NOTHROW(my_allocator_clean_up(my_json::json_value(t).array));
next_construct_fails = true;
CHECK_THROWS_AS(my_json::json_value(t), std::bad_alloc&);
next_construct_fails = false;
}
SECTION("string")
{
next_construct_fails = false;
auto t = my_json::value_t::string;
CHECK_NOTHROW(my_allocator_clean_up(my_json::json_value(t).string));
next_construct_fails = true;
CHECK_THROWS_AS(my_json::json_value(t), std::bad_alloc&);
next_construct_fails = false;
}
}
SECTION("json_value(const string_t&)")
{
next_construct_fails = false;
const my_json::string_t v("foo");
CHECK_NOTHROW(my_allocator_clean_up(my_json::json_value(v).string));
next_construct_fails = true;
CHECK_THROWS_AS(my_json::json_value(v), std::bad_alloc&);
next_construct_fails = false;
}
}
SECTION("class basic_json")
{
SECTION("basic_json(const CompatibleObjectType&)")
{
next_construct_fails = false;
const std::map<std::string, std::string> v {{"foo", "bar"}};
CHECK_NOTHROW(my_json(v));
next_construct_fails = true;
CHECK_THROWS_AS(my_json(v), std::bad_alloc&);
next_construct_fails = false;
}
SECTION("basic_json(const CompatibleArrayType&)")
{
next_construct_fails = false;
const std::vector<std::string> v {"foo", "bar", "baz"};
CHECK_NOTHROW(my_json(v));
next_construct_fails = true;
CHECK_THROWS_AS(my_json(v), std::bad_alloc&);
next_construct_fails = false;
}
SECTION("basic_json(const typename string_t::value_type*)")
{
next_construct_fails = false;
CHECK_NOTHROW(my_json("foo"));
next_construct_fails = true;
CHECK_THROWS_AS(my_json("foo"), std::bad_alloc&);
next_construct_fails = false;
}
SECTION("basic_json(const typename string_t::value_type*)")
{
next_construct_fails = false;
const std::string s("foo");
CHECK_NOTHROW(my_json(s));
next_construct_fails = true;
CHECK_THROWS_AS(my_json(s), std::bad_alloc&);
next_construct_fails = false;
}
SECTION("basic_json(const basic_json&) of a deeply nested value (#5387)")
{
// Copying a value nested deeper than the descent bound builds the
// copy from the top down: every value whose own copy has not been
// made yet stays a null value until it is. Failing an allocation
// part-way through is what proves such a half-built copy can still
// be destroyed.
//
// Which path the failure lands in depends on the build: the first
// allocation of a copy belongs to the outermost level, so here it
// is the descending one. Built with JSON_NO_THREAD_LOCAL - as the
// ci_test_no_thread_local target builds the whole suite - no
// descent is made at all and the very same failure lands in the
// iterative path instead, part-way through its worklist.
const auto check_deep_copy = [](bool objects)
{
CAPTURE(objects)
next_construct_fails = false;
// deeper than the 128 levels the copy constructor descends into
const std::size_t depth = 300;
my_json j = 1;
for (std::size_t i = 0; i < depth; ++i)
{
if (objects)
{
my_json wrapper = my_json::object();
wrapper["a"] = std::move(j);
j = std::move(wrapper);
}
else
{
j = my_json::array({std::move(j)});
}
}
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is tested
CHECK_NOTHROW(my_json(j));
next_construct_fails = true;
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is tested
CHECK_THROWS_AS(my_json(j), std::bad_alloc&);
next_construct_fails = false;
};
check_deep_copy(false);
check_deep_copy(true);
}
}
}
namespace
{
// counts every allocation made on behalf of a basic_json value (of its own
// object_t/array_t/string_t/binary_t or of its own type), and can be told to
// fail one of them: the n-th call to allocate() throws std::bad_alloc instead
// of allocating, whichever type it is allocating for
std::size_t alloc_call_count = 0;
long fail_at_alloc_call = -1; // -1: never fail
template<class T>
struct nth_alloc_fails_allocator : std::allocator<T>
{
using std::allocator<T>::allocator;
T* allocate(std::size_t n)
{
const auto index = alloc_call_count++;
if (fail_at_alloc_call >= 0 && index == static_cast<std::size_t>(fail_at_alloc_call))
{
throw std::bad_alloc();
}
return std::allocator<T>::allocate(n);
}
template <class U>
struct rebind
{
using other = nth_alloc_fails_allocator<U>;
};
};
// builds a value nested more than 128 levels deep - the bound the copy
// constructor descends into before it continues without the call stack - and
// checks that a copy survives any single allocation of it failing: every
// attempt either throws std::bad_alloc, without crashing or leaving the
// source altered, or completes the copy
template<class BasicJsonType>
void check_deep_copy_survives_failing_allocation(bool nest_objects)
{
CAPTURE(nest_objects)
fail_at_alloc_call = -1;
// [[[ ... [1] ... ]]], or the same nesting with objects, 130 levels deep
BasicJsonType src = 1;
for (std::size_t i = 0; i < 130; ++i)
{
if (nest_objects)
{
BasicJsonType wrapper = BasicJsonType::object();
wrapper["a"] = std::move(src);
src = std::move(wrapper);
}
else
{
src = BasicJsonType::array({std::move(src)});
}
}
const std::string original_dump = src.dump();
// first measure how many allocations an unhindered copy takes
alloc_call_count = 0;
{
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is measured
const BasicJsonType measure(src);
}
const std::size_t total_allocations = alloc_call_count;
REQUIRE(total_allocations > 0);
REQUIRE(src.dump() == original_dump);
// let the 0th, 1st, 2nd, ... allocation of the copy fail in turn; every
// such copy must throw std::bad_alloc rather than crash, and the source
// must come out exactly as it went in
for (std::size_t n = 0; n < total_allocations; ++n)
{
CAPTURE(n)
alloc_call_count = 0;
fail_at_alloc_call = static_cast<long>(n);
CHECK_THROWS_AS(BasicJsonType(src), std::bad_alloc&);
fail_at_alloc_call = -1;
CHECK(src.dump() == original_dump);
}
// once no allocation is made to fail, the copy itself must succeed
fail_at_alloc_call = -1;
const BasicJsonType copy(src);
CHECK(copy.dump() == original_dump);
CHECK(src.dump() == original_dump);
}
} // namespace
TEST_CASE("copy of a deeply nested value survives a failing allocation (#5640)")
{
// With iterator debugging (MSVC STL debug builds, also used by clang-cl),
// containers allocate a debug proxy through the allocator inside their
// noexcept move constructors, so failing that allocation terminates the
// program instead of throwing std::bad_alloc. Nothing to check there.
#if !(defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
SECTION("std::map-backed object_t")
{
using bad_alloc_json = nlohmann::json::with_allocator_t<nth_alloc_fails_allocator>;
check_deep_copy_survives_failing_allocation<bad_alloc_json>(false);
check_deep_copy_survives_failing_allocation<bad_alloc_json>(true);
}
SECTION("ordered_map-backed object_t")
{
using bad_alloc_ordered_json = nlohmann::ordered_json::with_allocator_t<nth_alloc_fails_allocator>;
check_deep_copy_survives_failing_allocation<bad_alloc_ordered_json>(false);
check_deep_copy_survives_failing_allocation<bad_alloc_ordered_json>(true);
}
#endif
}
namespace
{
// counts the allocations of pairs with a non-const first member: the object
// types store std::pair<const Key, T>, so only the scratch space of the
// iterative deep copy allocates std::pair<Key, T>
std::size_t scratch_pair_allocations = 0;
template<class T>
struct is_scratch_pair : std::false_type {};
template<class K, class V>
struct is_scratch_pair<std::pair<K, V>> : std::integral_constant < bool, !std::is_const<K>::value > {};
template<class T>
struct scratch_counting_allocator : std::allocator<T>
{
using std::allocator<T>::allocator;
T* allocate(std::size_t n)
{
if (is_scratch_pair<T>::value)
{
++scratch_pair_allocations;
}
return std::allocator<T>::allocate(n);
}
#ifdef __cpp_lib_allocate_at_least
// std::allocator<T>::allocate_at_least would bypass the counting, and
// libc++'s containers prefer it over allocate from C++23 on
auto allocate_at_least(std::size_t n)
{
if (is_scratch_pair<T>::value)
{
++scratch_pair_allocations;
}
return std::allocator<T>::allocate_at_least(n);
}
#endif
template <class U>
struct rebind
{
using other = scratch_counting_allocator<U>;
};
};
} // namespace
TEST_CASE("deep copy uses the provided allocator")
{
using counting_json = nlohmann::json::with_allocator_t<scratch_counting_allocator>;
// deeper than the 128 levels the copy constructor descends into, so the
// innermost objects are copied by the iterative deep copy
counting_json j = 1;
for (std::size_t i = 0; i < 300; ++i)
{
counting_json wrapper = counting_json::object();
wrapper["a"] = std::move(j);
j = std::move(wrapper);
}
scratch_pair_allocations = 0;
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is tested
const counting_json copy(j);
CHECK(scratch_pair_allocations > 0);
CHECK(copy == j);
}
namespace
{
// the number of constructions countdown_allocator lets happen, including the
// one that fails; 0 means none ever fails
std::size_t constructions_until_failure = 0;
template<class T>
struct countdown_allocator : std::allocator<T>
{
using std::allocator<T>::allocator;
template<class U, class... Args>
void construct(U* p, Args&& ... args)
{
if (constructions_until_failure != 0)
{
--constructions_until_failure;
if (constructions_until_failure == 0)
{
throw std::bad_alloc();
}
}
::new (static_cast<void*>(p)) U(std::forward<Args>(args)...);
}
template <class U>
struct rebind
{
using other = countdown_allocator<U>;
};
};
} // namespace
TEST_CASE("converting a deeply nested value from another specialization fails cleanly (#5650)")
{
// MSVC 2015's debug STL constructs the containers' debug proxies through
// the allocator in noexcept constructors, so a failing construction crashes
// the program there instead of throwing std::bad_alloc. Nothing to check.
#if !(defined(_MSC_VER) && _MSC_VER < 1910 && defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
using countdown_json = nlohmann::json::with_allocator_t<countdown_allocator>;
// deeper than the 128 levels the converting constructor descends into, so
// that failures land on both sides of the bound - or, built with
// JSON_NO_THREAD_LOCAL, all in the iterative conversion
json j = {1, "two", {{"three", 3}}};
for (std::size_t i = 0; i < 150; ++i)
{
j = json{{"a", json::array({j, "sibling"})}};
}
// Fail every construction in turn. Each failure has to reach the caller,
// and everything built until then has to be destroyed cleanly.
std::size_t failures = 0;
for (std::size_t n = 1;; ++n)
{
constructions_until_failure = n;
try
{
const countdown_json converted = j;
constructions_until_failure = 0;
CHECK(converted.dump() == j.dump());
break;
}
catch (const std::bad_alloc&)
{
++failures;
}
}
CHECK(failures > 0);
#endif
}
namespace
{
template<class T>
struct allocator_no_forward : std::allocator<T>
{
allocator_no_forward() = default;
template <class U>
allocator_no_forward(const allocator_no_forward<U>& /*unused*/) {}
template <class U>
struct rebind
{
using other = allocator_no_forward<U>;
};
template <class... Args>
void construct(T* p, const Args& ... args) noexcept(noexcept(::new (static_cast<void*>(p)) T(args...)))
{
// force copy even if move is available
::new (static_cast<void*>(p)) T(args...);
}
};
} // namespace
TEST_CASE("bad my_allocator::construct")
{
SECTION("my_allocator::construct doesn't forward")
{
using bad_alloc_json = nlohmann::json::with_allocator_t<allocator_no_forward>;
bad_alloc_json j;
j["test"] = bad_alloc_json::array_t();
j["test"].push_back("should not leak");
}
}
namespace
{
std::size_t counting_allocator_allocations = 0;
std::size_t counting_allocator_deallocations = 0;
template<class T>
struct counting_allocator : std::allocator<T>
{
using std::allocator<T>::allocator;
T* allocate(std::size_t n)
{
++counting_allocator_allocations;
return std::allocator<T>::allocate(n);
}
void deallocate(T* p, std::size_t n)
{
++counting_allocator_deallocations;
std::allocator<T>::deallocate(p, n);
}
template <class U>
struct rebind
{
using other = counting_allocator<U>;
};
};
} // namespace
TEST_CASE("destructor performs no allocation, only deallocation")
{
// see https://github.com/nlohmann/json/issues/4842 and
// https://github.com/nlohmann/json/issues/5135: destroying nested
// arrays/objects used to allocate a temporary stack (first with
// std::allocator, later - after #4842 - with the provided allocator).
// Since that stack could itself throw bad_alloc from inside the
// noexcept destructor (#5135), destroy() no longer allocates anything:
// it only ever frees what is already there.
using counting_json = nlohmann::json::with_allocator_t<counting_allocator>;
SECTION("array")
{
auto* j = new counting_json({1, {2, {3, 4}}, 5}); // NOLINT(cppcoreguidelines-owning-memory)
const auto allocations_before = counting_allocator_allocations;
const auto deallocations_before = counting_allocator_deallocations;
delete j; // NOLINT(cppcoreguidelines-owning-memory)
CHECK(counting_allocator_allocations == allocations_before);
CHECK(counting_allocator_deallocations > deallocations_before);
}
SECTION("object")
{
auto* j = new counting_json({{"a", {{"b", {1, 2}}}}, {"c", 3}}); // NOLINT(cppcoreguidelines-owning-memory)
const auto allocations_before = counting_allocator_allocations;
const auto deallocations_before = counting_allocator_deallocations;
delete j; // NOLINT(cppcoreguidelines-owning-memory)
CHECK(counting_allocator_allocations == allocations_before);
CHECK(counting_allocator_deallocations > deallocations_before);
}
SECTION("mixed tree of empty/non-empty arrays and objects")
{
auto* j = new counting_json( // NOLINT(cppcoreguidelines-owning-memory)
{
{"empty_obj", counting_json::object()},
{"empty_arr", counting_json::array()},
{"nested", {{"a", counting_json::array({1, 2, counting_json::object()})}, {"b", 3}}},
{"tail", counting_json::array({counting_json::array({1}), 2, counting_json::array({3})})}
});
const auto allocations_before = counting_allocator_allocations;
const auto deallocations_before = counting_allocator_deallocations;
delete j; // NOLINT(cppcoreguidelines-owning-memory)
CHECK(counting_allocator_allocations == allocations_before);
CHECK(counting_allocator_deallocations > deallocations_before);
}
}
// the no-exceptions CI job skips every CHECK_THROWS_AS, which would leave
// next_construct_fails set for the next allocation outside a check
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("a failed allocation leaves the value unchanged")
{
// create JSON type using the throwing allocator
using my_json = nlohmann::json::with_allocator_t<my_allocator>;
// Each of these creates a string, array, object, or binary value. The
// value must be created before the type is changed: otherwise, a failed
// creation left a value of the new type without anything behind it (an
// assertion in its destructor, a null pointer everywhere else) or, when
// an old value was destroyed first, with a pointer to that destroyed one.
SECTION("creating a binary value")
{
const std::vector<std::uint8_t> bytes = {1, 2, 3};
my_json _;
next_construct_fails = true;
CHECK_THROWS_AS(_ = my_json::binary(bytes), std::bad_alloc&);
next_construct_fails = true;
CHECK_THROWS_AS(_ = my_json::binary(bytes, 42), std::bad_alloc&);
next_construct_fails = true;
CHECK_THROWS_AS(_ = my_json::binary(std::vector<std::uint8_t>(bytes)), std::bad_alloc&);
next_construct_fails = true;
CHECK_THROWS_AS(_ = my_json::binary(std::vector<std::uint8_t>(bytes), 42), std::bad_alloc&);
next_construct_fails = false;
}
SECTION("turning a null value into an array or object")
{
my_json j;
next_construct_fails = true;
CHECK_THROWS_AS(j[0], std::bad_alloc&);
CHECK(j.is_null());
next_construct_fails = true;
CHECK_THROWS_AS(j["key"], std::bad_alloc&);
CHECK(j.is_null());
next_construct_fails = true;
CHECK_THROWS_AS(j.push_back(my_json(1)), std::bad_alloc&);
CHECK(j.is_null());
const my_json one = 1;
next_construct_fails = true;
CHECK_THROWS_AS(j.push_back(one), std::bad_alloc&);
CHECK(j.is_null());
next_construct_fails = true;
CHECK_THROWS_AS(j.push_back(my_json::object_t::value_type("key", 1)), std::bad_alloc&);
CHECK(j.is_null());
next_construct_fails = true;
CHECK_THROWS_AS(j.emplace_back(1), std::bad_alloc&);
CHECK(j.is_null());
next_construct_fails = true;
CHECK_THROWS_AS(j.emplace("key", 1), std::bad_alloc&);
CHECK(j.is_null());
const my_json object = {{"key", 1}};
next_construct_fails = true;
CHECK_THROWS_AS(j.update(object), std::bad_alloc&);
CHECK(j.is_null());
next_construct_fails = false;
}
// With iterator debugging, VS 2015's containers construct a proxy with the
// allocator in constructors that cannot report its failure, so a failing
// allocator crashes this section there (SIGSEGV with VS 2015 Debug x86).
#if !(defined(_MSC_VER) && _MSC_VER < 1910 && defined(_ITERATOR_DEBUG_LEVEL) && _ITERATOR_DEBUG_LEVEL > 0)
SECTION("converting into an existing value")
{
// to_json replaces the value it is given; the old one must survive a
// failed creation of the new one
my_json j = "old";
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, std::string("new")), std::bad_alloc&);
CHECK(j == "old");
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, std::vector<int> {1, 2}), std::bad_alloc&);
CHECK(j == "old");
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, std::vector<bool> {true, false}), std::bad_alloc&);
CHECK(j == "old");
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, std::map<std::string, int> {{"a", 1}}), std::bad_alloc&);
CHECK(j == "old");
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, my_json::binary_t({1, 2})), std::bad_alloc&);
CHECK(j == "old");
// the overloads for lvalues of the value types, for the value types
// themselves, and for the remaining compatible types
const std::string string = "new";
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, string), std::bad_alloc&);
CHECK(j == "old");
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, "new"), std::bad_alloc&);
CHECK(j == "old");
// to_json only moves a binary value that it converted from another
// container type, which my_json's std::vector<std::uint8_t> is not
using binary_constructor = nlohmann::detail::external_constructor<nlohmann::detail::value_t::binary>;
next_construct_fails = true;
CHECK_THROWS_AS(binary_constructor::construct(j, my_json::binary_t({1, 2})), std::bad_alloc&);
CHECK(j == "old");
my_json::array_t array = {1, 2};
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, array), std::bad_alloc&);
CHECK(j == "old");
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, std::move(array)), std::bad_alloc&);
CHECK(j == "old");
my_json::object_t object = {{"a", 1}};
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, object), std::bad_alloc&);
CHECK(j == "old");
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, std::move(object)), std::bad_alloc&);
CHECK(j == "old");
next_construct_fails = true;
CHECK_THROWS_AS(nlohmann::to_json(j, std::valarray<int> {1, 2}), std::bad_alloc&);
CHECK(j == "old");
next_construct_fails = false;
nlohmann::to_json(j, std::vector<int> {1, 2});
CHECK(j == my_json({1, 2}));
}
#endif
}
#endif