mirror of
https://github.com/nlohmann/json.git
synced 2026-10-03 21:20:30 +00:00
Let to_json(std::optional<T>) propagate exceptions from T's to_json (#5684)
* Let to_json(std::optional<T>) propagate exceptions from T's to_json The overload was marked noexcept even though its body assigns *opt to the JSON value, which calls T's to_json (or allocates for std::string, std::vector, or json). Any exception from there -- a user-defined to_json reporting an error, or std::bad_alloc -- called std::terminate() instead of propagating. The noexcept also made basic_json's converting constructor noexcept(true) for std::optional<T>, so json j = opt; could not report the error either. Fixes #5642. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix CI: make to_json(std::optional<T>) conditionally noexcept GCC's -Wnoexcept (an error in ci_test_gcc) fired at to_json_fn's noexcept(noexcept(to_json(j, val))): after dropping the unconditional noexcept, to_json(std::optional<int>) had no exception specification although GCC could prove its body cannot throw. It also made json(std::optional<int>) lose its noexcept. Declare the overload noexcept exactly when assigning the contained value to the JSON value is (std::is_nothrow_assignable<BasicJsonType&, const T&>), which is what the body does. std::optional<int> is noexcept again; a T whose to_json may throw still propagates the exception. Static assertions in the test check both cases. The test's throwing to_json triggered -Wmissing-prototypes and -Wmissing-noreturn (clang) and -Wmissing-declarations and -Wsuggest-attribute=noreturn (GCC). Move the type and its to_json into an anonymous namespace, mark the function [[noreturn]], and compile them only without JSON_NOEXCEPTION, like the test that uses them. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
@@ -1834,6 +1834,21 @@ TEST_CASE("std::u8string")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
namespace
|
||||
{
|
||||
// a type whose to_json reports an error by throwing, used below to check that
|
||||
// converting a std::optional<T> to JSON propagates an exception thrown while
|
||||
// converting its contained value instead of calling std::terminate (#5642)
|
||||
struct throwing_to_json_type {};
|
||||
|
||||
[[noreturn]] void to_json(json& /*unused*/, const throwing_to_json_type& /*unused*/)
|
||||
{
|
||||
throw std::runtime_error("cannot serialize throwing_to_json_type");
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
TEST_CASE("std::optional")
|
||||
{
|
||||
SECTION("null")
|
||||
@@ -1916,6 +1931,23 @@ TEST_CASE("std::optional")
|
||||
CHECK(json(opt_object) == j_object);
|
||||
CHECK(std::map<std::string, std::optional<int>>(j_object) == opt_object);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("exception from contained value's to_json propagates (#5642)")
|
||||
{
|
||||
// to_json(BasicJsonType&, const std::optional<T>&) must not be
|
||||
// noexcept: it calls T's to_json, which may throw (a user-defined
|
||||
// to_json that reports an error, or std::bad_alloc for T =
|
||||
// std::string/vector/json). Before the fix, this called
|
||||
// std::terminate() instead of letting the exception propagate.
|
||||
const std::optional<throwing_to_json_type> opt = throwing_to_json_type{};
|
||||
CHECK_THROWS_WITH_AS(json(opt), "cannot serialize throwing_to_json_type", std::runtime_error&);
|
||||
|
||||
// the conversion is noexcept exactly when converting the contained value is
|
||||
static_assert(!std::is_nothrow_constructible<json, const std::optional<throwing_to_json_type>&>::value, "");
|
||||
static_assert(std::is_nothrow_constructible<json, const std::optional<int>&>::value, "");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user