diff --git a/include/xtensor/utils/xexception.hpp b/include/xtensor/utils/xexception.hpp index 025836ce2..d51146b44 100644 --- a/include/xtensor/utils/xexception.hpp +++ b/include/xtensor/utils/xexception.hpp @@ -340,6 +340,17 @@ namespace xt #define XTENSOR_ASSERT(expr) #endif + namespace detail + { + // True when a failing XTENSOR_ASSERT throws. A function containing an + // XTENSOR_ASSERT can only be noexcept when this is false. +#if defined(XTENSOR_ENABLE_ASSERT) && !defined(XTENSOR_DISABLE_EXCEPTIONS) + inline constexpr bool assert_can_throw = true; +#else + inline constexpr bool assert_can_throw = false; +#endif + } + #ifdef XTENSOR_ENABLE_CHECK_DIMENSION #define XTENSOR_CHECK_DIMENSION(S, ARGS) XTENSOR_TRY(check_dimension(S, ARGS)) #else diff --git a/include/xtensor/utils/xutils.hpp b/include/xtensor/utils/xutils.hpp index ed4a04f48..5b1c2c405 100644 --- a/include/xtensor/utils/xutils.hpp +++ b/include/xtensor/utils/xutils.hpp @@ -28,6 +28,7 @@ #include #include "../core/xtensor_config.hpp" +#include "xexception.hpp" namespace xt { @@ -47,8 +48,22 @@ namespace xt template constexpr decltype(auto) argument(Args&&... args) noexcept; + namespace detail + { + // Whether calling F on each element type S and converting the result to R cannot throw. + template + inline constexpr bool + is_nothrow_apply_call_v = (noexcept(static_cast(std::declval()(std::declval()))) && ...); + + // apply also checks the index and falls back to R{}. + template + inline constexpr bool is_nothrow_apply_v = is_nothrow_apply_call_v + && std::is_nothrow_default_constructible::value + && !assert_can_throw; + } + template - R apply(std::size_t index, F&& func, const std::tuple& s) noexcept(noexcept(func(std::get<0>(s)))); + R apply(std::size_t index, F&& func, const std::tuple& s) noexcept(detail::is_nothrow_apply_v); template void nested_copy(T&& iter, const S& s); @@ -252,7 +267,7 @@ namespace xt template inline R - apply(std::size_t index, F&& func, const std::tuple& s) noexcept(noexcept(func(std::get<0>(s)))) + apply(std::size_t index, F&& func, const std::tuple& s) noexcept(detail::is_nothrow_apply_v) { XTENSOR_ASSERT(sizeof...(S) > index); return std::apply( diff --git a/test/test_xutils.cpp b/test/test_xutils.cpp index 6107c4bce..db2b2ba5c 100644 --- a/test/test_xutils.cpp +++ b/test/test_xutils.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include @@ -113,9 +115,44 @@ namespace xt return i; }; auto t = std::make_tuple(1, 2, 3); - static_assert(noexcept(apply(1, func_ne, t))); + // The bound check on the index throws when assertions are enabled. + static_assert(noexcept(apply(1, func_ne, t)) == !detail::assert_can_throw); } + struct throw_on_string + { + int operator()(int i) const noexcept + { + return i; + } + + int operator()(const std::string&) const + { + throw std::runtime_error("throw_on_string"); + } + }; + + TEST(utils, apply_propagates_exception) + { + // Only the call on the first element is noexcept: apply must not be. + auto t = std::make_tuple(1, std::string("a")); + static_assert(!noexcept(apply(1, throw_on_string{}, t))); + EXPECT_EQ(apply(0, throw_on_string{}, t), 1); + XT_EXPECT_THROW(apply(1, throw_on_string{}, t), std::runtime_error); + } + +#ifdef XTENSOR_ENABLE_ASSERT + TEST(utils, apply_out_of_range) + { + auto func_ne = [](int i) noexcept + { + return i; + }; + auto t = std::make_tuple(1, 2, 3); + XT_EXPECT_THROW(apply(3, func_ne, t), std::runtime_error); + } +#endif + TEST(utils, conditional_cast) { EXPECT_TRUE((std::is_same(1)), int>::value));