From 2234cd2d4040b3bdedafaaf2e96fa016d505745c Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 5 Oct 2026 20:08:17 +0200 Subject: [PATCH] Fix std::terminate when converting std::optional with MSVC 2017 MSVC 2017 evaluates std::is_nothrow_assignable as true even if T's to_json throws, so to_json(json&, const std::optional&) was noexcept there and the exception from #5642's test called std::terminate instead of propagating. Make that conversion never noexcept on MSVC 2017; all other compilers keep the exact condition. The static_asserts on the condition are skipped for MSVC 2017; the runtime check that the exception propagates still runs there. Signed-off-by: Niels Lohmann --- include/nlohmann/detail/conversions/to_json.hpp | 13 ++++++++++++- single_include/nlohmann/json.hpp | 13 ++++++++++++- tests/src/unit-conversions2.cpp | 6 +++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/include/nlohmann/detail/conversions/to_json.hpp b/include/nlohmann/detail/conversions/to_json.hpp index d7197f323..f3b4994f1 100644 --- a/include/nlohmann/detail/conversions/to_json.hpp +++ b/include/nlohmann/detail/conversions/to_json.hpp @@ -287,9 +287,20 @@ struct external_constructor ///////////// #ifdef JSON_HAS_CPP_17 +// whether storing the value of a std::optional cannot throw; MSVC 2017 +// evaluates std::is_nothrow_assignable as true even if T's to_json throws, so +// the exception would call std::terminate (#5642) +#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER < 1920 + template + using is_nothrow_optional_to_json = std::false_type; +#else + template + using is_nothrow_optional_to_json = std::is_nothrow_assignable; +#endif + template::value, int> = 0> -void to_json(BasicJsonType& j, const std::optional& opt) noexcept(std::is_nothrow_assignable::value) +void to_json(BasicJsonType& j, const std::optional& opt) noexcept(is_nothrow_optional_to_json::value) { if (opt.has_value()) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 765095dcb..d0a651093 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -7081,9 +7081,20 @@ struct external_constructor ///////////// #ifdef JSON_HAS_CPP_17 +// whether storing the value of a std::optional cannot throw; MSVC 2017 +// evaluates std::is_nothrow_assignable as true even if T's to_json throws, so +// the exception would call std::terminate (#5642) +#if defined(_MSC_VER) && !defined(__clang__) && _MSC_VER < 1920 + template + using is_nothrow_optional_to_json = std::false_type; +#else + template + using is_nothrow_optional_to_json = std::is_nothrow_assignable; +#endif + template::value, int> = 0> -void to_json(BasicJsonType& j, const std::optional& opt) noexcept(std::is_nothrow_assignable::value) +void to_json(BasicJsonType& j, const std::optional& opt) noexcept(is_nothrow_optional_to_json::value) { if (opt.has_value()) { diff --git a/tests/src/unit-conversions2.cpp b/tests/src/unit-conversions2.cpp index ed3103484..c543e4ff8 100644 --- a/tests/src/unit-conversions2.cpp +++ b/tests/src/unit-conversions2.cpp @@ -853,11 +853,11 @@ TEST_CASE("std::optional") 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 - // (MSVC 2017 evaluates the conditional noexcept of this conversion as true) -#if !defined(_MSC_VER) || _MSC_VER >= 1920 + // (except with MSVC 2017, where it is never noexcept, see to_json.hpp) +#if !defined(_MSC_VER) || defined(__clang__) || _MSC_VER >= 1920 static_assert(!std::is_nothrow_constructible&>::value); -#endif static_assert(std::is_nothrow_constructible&>::value); +#endif } #endif }