diff --git a/include/fn/choice.hpp b/include/fn/choice.hpp index 39889d86..6c0ebddd 100644 --- a/include/fn/choice.hpp +++ b/include/fn/choice.hpp @@ -86,8 +86,9 @@ struct choice : sum { */ template constexpr choice(T &&v) // NOSONAR cpp:S1709,S6458 implicit arm of the explicit pair; has_type excludes self - noexcept(detail::_nothrow_initializable<::std::remove_cvref_t, decltype(v)>) - requires has_type<::std::remove_cvref_t> && (::std::is_constructible_v<::std::remove_cvref_t, decltype(v)>) + noexcept(::std::is_nothrow_constructible_v<_impl, ::std::in_place_type_t<::std::remove_cvref_t>, decltype(v)>) + requires has_type<::std::remove_cvref_t> + && (::std::is_constructible_v<_impl, ::std::in_place_type_t<::std::remove_cvref_t>, decltype(v)>) && (::std::is_convertible_v>) : _impl(::std::in_place_type<::std::remove_cvref_t>, FWD(v)) { @@ -101,8 +102,9 @@ struct choice : sum { */ template constexpr explicit choice(T &&v) // NOSONAR cpp:S6458 has_type excludes self - noexcept(detail::_nothrow_initializable<::std::remove_cvref_t, decltype(v)>) - requires has_type<::std::remove_cvref_t> && (::std::is_constructible_v<::std::remove_cvref_t, decltype(v)>) + noexcept(::std::is_nothrow_constructible_v<_impl, ::std::in_place_type_t<::std::remove_cvref_t>, decltype(v)>) + requires has_type<::std::remove_cvref_t> + && (::std::is_constructible_v<_impl, ::std::in_place_type_t<::std::remove_cvref_t>, decltype(v)>) && (not ::std::is_convertible_v>) : _impl(::std::in_place_type<::std::remove_cvref_t>, FWD(v)) { @@ -117,8 +119,8 @@ struct choice : sum { */ template constexpr explicit choice(::std::in_place_type_t d, auto &&...args) // - noexcept(detail::_nothrow_initializable) - requires has_type && detail::_initializable + noexcept(::std::is_nothrow_constructible_v<_impl, ::std::in_place_type_t, decltype(args)...>) + requires has_type && ::std::is_constructible_v<_impl, ::std::in_place_type_t, decltype(args)...> : _impl(d, FWD(args)...) { } @@ -131,8 +133,9 @@ struct choice : sum { */ template constexpr choice(sum const &v) // NOSONAR cpp:S1709 implicit widening by design - noexcept((... && ::std::is_nothrow_copy_constructible_v)) - requires detail::is_superset_of> && (... && ::std::is_copy_constructible_v) + noexcept(::std::is_nothrow_constructible_v<_impl, ::std::in_place_type_t>, sum const &>) + requires detail::is_superset_of> + && (::std::is_constructible_v<_impl, ::std::in_place_type_t>, sum const &>) : _impl(::std::in_place_type>, FWD(v)) { } @@ -145,8 +148,9 @@ struct choice : sum { */ template constexpr choice(sum &&v) // NOSONAR cpp:S1709 implicit widening by design - noexcept((... && ::std::is_nothrow_move_constructible_v)) - requires detail::is_superset_of> && (... && ::std::is_move_constructible_v) + noexcept(::std::is_nothrow_constructible_v<_impl, ::std::in_place_type_t>, sum>) + requires detail::is_superset_of> + && (::std::is_constructible_v<_impl, ::std::in_place_type_t>, sum>) : _impl(::std::in_place_type>, FWD(v)) { } @@ -159,7 +163,7 @@ struct choice : sum { */ template constexpr choice(::std::in_place_type_t>, some_sum auto &&v) // - noexcept((... && ::std::is_nothrow_constructible_v>)) + noexcept(::std::is_nothrow_constructible_v<_impl, ::std::in_place_type_t>, decltype(v)>) requires ::std::is_same_v<::std::remove_cvref_t, sum> && detail::is_superset_of> : _impl(::std::in_place_type>, FWD(v)) diff --git a/include/fn/detail/pack_impl.hpp b/include/fn/detail/pack_impl.hpp index bc7adeaa..d122c032 100644 --- a/include/fn/detail/pack_impl.hpp +++ b/include/fn/detail/pack_impl.hpp @@ -27,7 +27,9 @@ template <::std::size_t I, typename T> struct _element { // type is reference-related to it, i.e. binds), but gcc reads the braced form as materializing a // temporary and rejects the bind - while accepting the equivalent declaration. The cast keeps both // compilers on the binding path. -template [[nodiscard]] constexpr auto _make_element(auto &&...args) -> T +template +[[nodiscard]] constexpr auto _make_element(auto &&...args) noexcept(_nothrow_initializable) -> T + requires _initializable { if constexpr (::std::is_reference_v) return T(FWD(args)...); // a single argument, so this is a cast expression: it binds @@ -35,6 +37,25 @@ template [[nodiscard]] constexpr auto _make_element(auto &&...args) return T{FWD(args)...}; } +// The two questions anyone above may ask about constructing an element, asked OF the function that +// constructs it rather than restated in terms of a trait - the same discipline as `_makeable` beside +// `make_variadic_union`, and for the same reason: a restatement can drift from the deed. +template +concept _makeable_element = requires { _make_element(::std::declval()...); }; + +template +concept _nothrow_makeable_element = requires { requires noexcept(_make_element(::std::declval()...)); }; + +// One element's relocation into a new pack: the copy-initialization its holder performs +// ([dcl.init.aggr]/4.3, reached through brace elision), asked of the holder one element at a time - +// `_element{src}` elides into the same member copy-initialization. This excludes explicit +// constructors, where `is_[nothrow_]constructible_v` would admit them. +template +concept _relocatable_element = requires { E{::std::declval()}; }; + +template +concept _nothrow_relocatable_element = requires { requires noexcept(E{::std::declval()}); }; + template struct pack_impl; template struct _pack_append; @@ -89,6 +110,16 @@ struct pack_impl<::std::index_sequence, Ts...> : _element... { FWD(fn), static_cast>(FWD(self)._element::v)..., FWD(args)...); } + template + requires(not(... || (_some_pack || _some_sum))) + static constexpr auto _invoke_r(Self &&self, Fn &&fn, Args &&...args) // + noexcept(_is_nothrow_invocable_r..., Args &&...>::value) -> Ret + requires(_is_invocable_r..., Args && ...>::value) + { + return ::fn::detail::_invoke_r( + FWD(fn), static_cast>(FWD(self)._element::v)..., FWD(args)...); + } + template <::std::size_t I, typename Self> static constexpr decltype(auto) _get(Self &&self) noexcept requires(I < size) @@ -101,15 +132,19 @@ struct pack_impl<::std::index_sequence, Ts...> : _element... { // Every existing element is relocated into the new pack, so appending weighs that as well as the // construction of the element being appended. + template + static constexpr bool _relocatable + = (... && _relocatable_element<_element, apply_const_lvalue_t>); + template static constexpr bool _nothrow_relocatable - = (... && ::std::is_nothrow_constructible_v>); + = (... && _nothrow_relocatable_element<_element, apply_const_lvalue_t>); template static constexpr auto _append(Self &&self, auto &&...args) // - noexcept(_nothrow_relocatable && _nothrow_initializable) + noexcept(_nothrow_relocatable && _nothrow_makeable_element) -> pack_impl<::std::index_sequence, Ts..., T> - requires(not _some_sum) && (not _some_pack) && _initializable + requires(not _some_sum) && (not _some_pack) && _relocatable && _makeable_element { return {static_cast>(FWD(self)._element::v)..., _make_element(FWD(args)...)}; @@ -121,6 +156,8 @@ struct pack_impl<::std::index_sequence, Ts...> : _element... { && ::std::remove_cvref_t::_impl::template _nothrow_relocatable) -> // typename _pack_append<::std::remove_cvref_t, Ts...>::impl requires _some_pack && (::std::is_same_v<::std::remove_cvref_t, ::std::remove_cvref_t>) + && _relocatable && ::std::remove_cvref_t::_impl::template + _relocatable { using type = _pack_append<::std::remove_cvref_t, Ts...>::impl; return FWD(other)._invoke(FWD(other), [&self](auto &&...args) { diff --git a/include/fn/detail/variadic_union.hpp b/include/fn/detail/variadic_union.hpp index a577e605..c8abda8d 100644 --- a/include/fn/detail/variadic_union.hpp +++ b/include/fn/detail/variadic_union.hpp @@ -306,40 +306,51 @@ template } template -[[nodiscard]] constexpr auto make_variadic_union(auto &&...args) - requires(U::template has_type) && ::std::is_same_v +[[nodiscard]] constexpr U make_variadic_union(auto &&...args) noexcept(noexcept(T{FWD(args)...})) + requires(U::template has_type) && ::std::is_same_v && requires { T{FWD(args)...}; } { return U{.v0 = T{FWD(args)...}}; } template -[[nodiscard]] constexpr auto make_variadic_union(auto &&...args) - requires(U::template has_type) && ::std::is_same_v +[[nodiscard]] constexpr U make_variadic_union(auto &&...args) noexcept(noexcept(T{FWD(args)...})) + requires(U::template has_type) && ::std::is_same_v && requires { T{FWD(args)...}; } { return U{.v1 = T{FWD(args)...}}; } template -[[nodiscard]] constexpr auto make_variadic_union(auto &&...args) - requires(U::template has_type) && ::std::is_same_v +[[nodiscard]] constexpr U make_variadic_union(auto &&...args) noexcept(noexcept(T{FWD(args)...})) + requires(U::template has_type) && ::std::is_same_v && requires { T{FWD(args)...}; } { return U{.v2 = T{FWD(args)...}}; } template -[[nodiscard]] constexpr auto make_variadic_union(auto &&...args) - requires(U::template has_type) && ::std::is_same_v +[[nodiscard]] constexpr U make_variadic_union(auto &&...args) noexcept(noexcept(T{FWD(args)...})) + requires(U::template has_type) && ::std::is_same_v && requires { T{FWD(args)...}; } { return U{.v3 = T{FWD(args)...}}; } template -[[nodiscard]] constexpr U make_variadic_union(auto &&...args) - requires(U::template has_type) && (U::more_t::template has_type) +[[nodiscard]] constexpr U make_variadic_union(auto &&...args) noexcept(noexcept(T{FWD(args)...})) + requires(U::template has_type) && (U::more_t::template has_type) && requires { T{FWD(args)...}; } { return U{.more = make_variadic_union(FWD(args)...)}; } +// The two questions anyone above may ask about storing an alternative, asked OF the function that +// stores it rather than restated in terms of a trait. Restating is how the answer drifts from the +// deed: `make_variadic_union` brace-initializes, and `std::is_[nothrow_]constructible` is a question +// about parentheses - the two can disagree, and where they do the trait is wrong. Nothing here needs +// to know that, and nothing above needs to remember it. +template +concept _makeable = requires { make_variadic_union(::std::declval()...); }; + +template +concept _nothrow_makeable = requires { requires noexcept(make_variadic_union(::std::declval()...)); }; + template [[nodiscard]] constexpr auto invoke_variadic_union(some_variadic_union auto &&v, ::std::size_t index, Fn &&fn, Args &&...args) diff --git a/include/fn/pack.hpp b/include/fn/pack.hpp index 4e10930c..5797cd2e 100644 --- a/include/fn/pack.hpp +++ b/include/fn/pack.hpp @@ -46,10 +46,8 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) & // - noexcept(_impl::template _nothrow_relocatable && detail::_nothrow_initializable) - -> append_type - requires detail::_initializable - && requires { append_type{_impl::template _append(*this, FWD(args)...)}; } + noexcept(noexcept(_impl::template _append(::std::declval(), FWD(args)...))) -> append_type + requires requires { append_type{_impl::template _append(*this, FWD(args)...)}; } { return {_impl::template _append(*this, FWD(args)...)}; } @@ -63,10 +61,8 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) const & // - noexcept(_impl::template _nothrow_relocatable - && detail::_nothrow_initializable) -> append_type - requires detail::_initializable - && requires { append_type{_impl::template _append(*this, FWD(args)...)}; } + noexcept(noexcept(_impl::template _append(::std::declval(), FWD(args)...))) -> append_type + requires requires { append_type{_impl::template _append(*this, FWD(args)...)}; } { return {_impl::template _append(*this, FWD(args)...)}; } @@ -80,10 +76,8 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) && // - noexcept(_impl::template _nothrow_relocatable && detail::_nothrow_initializable) - -> append_type - requires detail::_initializable - && requires { append_type{_impl::template _append(::std::move(*this), FWD(args)...)}; } + noexcept(noexcept(_impl::template _append(::std::declval(), FWD(args)...))) -> append_type + requires requires { append_type{_impl::template _append(::std::move(*this), FWD(args)...)}; } { return {_impl::template _append(::std::move(*this), FWD(args)...)}; } @@ -97,10 +91,8 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) const && // - noexcept(_impl::template _nothrow_relocatable - && detail::_nothrow_initializable) -> append_type - requires detail::_initializable - && requires { append_type{_impl::template _append(::std::move(*this), FWD(args)...)}; } + noexcept(noexcept(_impl::template _append(::std::declval(), FWD(args)...))) -> append_type + requires requires { append_type{_impl::template _append(::std::move(*this), FWD(args)...)}; } { return {_impl::template _append(::std::move(*this), FWD(args)...)}; } @@ -248,12 +240,10 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto invoke_r(Fn &&fn, auto &&...args) & // - noexcept(noexcept(_impl::_invoke(::std::declval(), FWD(fn), FWD(args)...)) - && ::std::is_nothrow_constructible_v) -> Ret - requires requires { _impl::_invoke(*this, FWD(fn), FWD(args)...); } - && ::std::is_convertible_v + noexcept(noexcept(_impl::template _invoke_r(::std::declval(), FWD(fn), FWD(args)...))) -> Ret + requires requires { _impl::template _invoke_r(*this, FWD(fn), FWD(args)...); } { - return _impl::_invoke(*this, FWD(fn), FWD(args)...); + return _impl::template _invoke_r(*this, FWD(fn), FWD(args)...); } /** @@ -267,12 +257,10 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto invoke_r(Fn &&fn, auto &&...args) const & // - noexcept(noexcept(_impl::_invoke(::std::declval(), FWD(fn), FWD(args)...)) - && ::std::is_nothrow_constructible_v) -> Ret - requires requires { _impl::_invoke(*this, FWD(fn), FWD(args)...); } - && ::std::is_convertible_v + noexcept(noexcept(_impl::template _invoke_r(::std::declval(), FWD(fn), FWD(args)...))) -> Ret + requires requires { _impl::template _invoke_r(*this, FWD(fn), FWD(args)...); } { - return _impl::_invoke(*this, FWD(fn), FWD(args)...); + return _impl::template _invoke_r(*this, FWD(fn), FWD(args)...); } /** @@ -286,13 +274,10 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto invoke_r(Fn &&fn, auto &&...args) && // - noexcept(noexcept(_impl::_invoke(::std::declval(), FWD(fn), FWD(args)...)) - && ::std::is_nothrow_constructible_v) -> Ret - requires requires { _impl::_invoke(::std::move(*this), FWD(fn), FWD(args)...); } - && ::std::is_convertible_v + noexcept(noexcept(_impl::template _invoke_r(::std::declval(), FWD(fn), FWD(args)...))) -> Ret + requires requires { _impl::template _invoke_r(::std::move(*this), FWD(fn), FWD(args)...); } { - return _impl::_invoke(::std::move(*this), FWD(fn), FWD(args)...); + return _impl::template _invoke_r(::std::move(*this), FWD(fn), FWD(args)...); } /** @@ -306,13 +291,10 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto invoke_r(Fn &&fn, auto &&...args) const && // - noexcept(noexcept(_impl::_invoke(::std::declval(), FWD(fn), FWD(args)...)) - && ::std::is_nothrow_constructible_v) -> Ret - requires requires { _impl::_invoke(::std::move(*this), FWD(fn), FWD(args)...); } - && ::std::is_convertible_v + noexcept(noexcept(_impl::template _invoke_r(::std::declval(), FWD(fn), FWD(args)...))) -> Ret + requires requires { _impl::template _invoke_r(::std::move(*this), FWD(fn), FWD(args)...); } { - return _impl::_invoke(::std::move(*this), FWD(fn), FWD(args)...); + return _impl::template _invoke_r(::std::move(*this), FWD(fn), FWD(args)...); } }; @@ -348,9 +330,9 @@ template <::std::size_t I, some_pack P> */ [[nodiscard]] constexpr auto as_pack() noexcept -> pack<> { return {}; } template - requires(not some_in_place_type) + requires(not some_in_place_type) && detail::_initializable, T, Args...> [[nodiscard]] constexpr auto as_pack(T &&src, Args &&...args) // - noexcept(::std::is_nothrow_constructible_v, T, Args...>) -> decltype(auto) + noexcept(detail::_nothrow_initializable, T, Args...>) -> pack { return pack{FWD(src), FWD(args)...}; } diff --git a/include/fn/sum.hpp b/include/fn/sum.hpp index 0c4991cc..a9211cf7 100644 --- a/include/fn/sum.hpp +++ b/include/fn/sum.hpp @@ -176,6 +176,22 @@ struct sum { static constexpr ::std::size_t size = sizeof...(Ts); + // What copying and moving a sum cost, asked of the storage that performs them - see the concepts + // in detail/variadic_union.hpp. Everything below is specified and constrained in these terms, so + // that no declaration has to restate what the storage does, and none can drift from it. + static constexpr bool _copyable = (... && detail::_makeable); + static constexpr bool _movable = (... && detail::_makeable); + static constexpr bool _nothrow_copyable = (... && detail::_nothrow_makeable); + static constexpr bool _nothrow_movable = (... && detail::_nothrow_makeable); + + // Copy assignment reinitializes ONE alternative, and `_reinit` chooses its arm per alternative - so + // the choice belongs INSIDE the fold. Hoisting it out would demand that one arm serve them all, and + // would reject a sum whose alternatives simply need different ones. + static constexpr bool _copy_assignable + = (... + && (detail::_makeable + && (detail::_nothrow_makeable || detail::_nothrow_makeable))); + /** * @brief TODO * @@ -235,8 +251,8 @@ struct sum { */ template constexpr sum(T &&v) // NOSONAR cpp:S1709,S6458 implicit arm of the explicit pair; has_type excludes self - noexcept(detail::_nothrow_initializable<::std::remove_cvref_t, decltype(v)>) - requires has_type<::std::remove_cvref_t> && (::std::is_constructible_v<::std::remove_cvref_t, decltype(v)>) + noexcept(detail::_nothrow_makeable, decltype(v)>) + requires has_type<::std::remove_cvref_t> && (detail::_makeable, decltype(v)>) && (::std::is_convertible_v>) : data(detail::make_variadic_union<::std::remove_cvref_t, data_t>(FWD(v))), index(detail::type_index<::std::remove_cvref_t, Ts...>) @@ -251,8 +267,8 @@ struct sum { */ template constexpr explicit sum(T &&v) // NOSONAR cpp:S6458 has_type excludes self - noexcept(detail::_nothrow_initializable<::std::remove_cvref_t, decltype(v)>) - requires has_type<::std::remove_cvref_t> && (::std::is_constructible_v<::std::remove_cvref_t, decltype(v)>) + noexcept(detail::_nothrow_makeable, decltype(v)>) + requires has_type<::std::remove_cvref_t> && (detail::_makeable, decltype(v)>) && (not ::std::is_convertible_v>) : data(detail::make_variadic_union<::std::remove_cvref_t, data_t>(FWD(v))), index(detail::type_index<::std::remove_cvref_t, Ts...>) @@ -267,8 +283,8 @@ struct sum { */ template constexpr explicit sum(::std::in_place_type_t, - auto &&...args) noexcept(detail::_nothrow_initializable) - requires has_type && detail::_initializable + auto &&...args) noexcept(detail::_nothrow_makeable) + requires has_type && detail::_makeable : data(detail::make_variadic_union(FWD(args)...)), index(detail::type_index) { } @@ -281,9 +297,9 @@ struct sum { */ template constexpr sum(sum const &arg) // NOSONAR cpp:S1709 implicit widening by design - noexcept((... && ::std::is_nothrow_copy_constructible_v)) + noexcept((... && detail::_nothrow_makeable)) requires detail::is_superset_of> && (not ::std::is_same_v>) - && (... && ::std::is_copy_constructible_v) && (sizeof...(Tx) > 0) + && (... && detail::_makeable) && (sizeof...(Tx) > 0) : data(FWD(arg).template _invoke([](::std::in_place_type_t, auto &&v) { return detail::make_variadic_union(FWD(v)); })), @@ -301,9 +317,9 @@ struct sum { */ template constexpr sum(sum &&arg) // NOSONAR cpp:S1709 implicit widening by design - noexcept((... && ::std::is_nothrow_move_constructible_v)) + noexcept((... && detail::_nothrow_makeable)) requires detail::is_superset_of> && (not ::std::is_same_v>) - && (... && ::std::is_move_constructible_v) && (sizeof...(Tx) > 0) + && (... && detail::_makeable) && (sizeof...(Tx) > 0) : data(FWD(arg).template _invoke([](::std::in_place_type_t, auto &&v) { return detail::make_variadic_union(FWD(v)); })), @@ -321,7 +337,7 @@ struct sum { */ template constexpr sum(::std::in_place_type_t>, some_sum auto &&arg) // - noexcept((... && ::std::is_nothrow_constructible_v>)) + noexcept((... && detail::_nothrow_makeable>)) requires ::std::is_same_v<::std::remove_cvref_t, sum> && detail::is_superset_of> && (sizeof...(Tx) > 0) : data(FWD(arg).template _invoke([](::std::in_place_type_t, auto &&v) { @@ -338,8 +354,8 @@ struct sum { * * @param other TODO */ - constexpr sum(sum const &other) noexcept((... && ::std::is_nothrow_copy_constructible_v)) - requires(... && ::std::is_copy_constructible_v) + constexpr sum(sum const &other) noexcept(_nothrow_copyable) + requires _copyable : data(detail::invoke_type_variadic_union( // other.data, other.index, // [](::std::in_place_type_t, auto const &v) { // @@ -354,8 +370,8 @@ struct sum { * * @param other TODO */ - constexpr sum(sum &&other) noexcept((... && ::std::is_nothrow_move_constructible_v)) - requires(... && ::std::is_move_constructible_v) + constexpr sum(sum &&other) noexcept(_nothrow_movable) + requires _movable : data(detail::invoke_type_variadic_union( // ::std::move(other).data, other.index, // [](::std::in_place_type_t, auto &&v) { // @@ -386,18 +402,24 @@ struct sum { // a possible OLD alternative (one is assigned over itself whenever the sum already holds it), and // snapshotting that same type would throw in turn. So there is no safe path, and such an // alternative is constrained away rather than half-served. + // + // The arm is chosen by asking the storage what it can do, never by a trait that restates it: + // `std::is_nothrow_move_constructible_v` asks about `T(T&&)`, the storage performs `T{...}`, and + // the two can disagree - braced initialization considers initializer-list constructors first, so a + // type can promise a nothrow move and still throw from `T{std::move(t)}`. Believing that promise + // here would destroy the old alternative and then fail to replace it. template - constexpr void _reinit(Args &&...args) noexcept(detail::_nothrow_initializable) - requires has_type && detail::_initializable - && (detail::_nothrow_initializable || ::std::is_nothrow_move_constructible_v) + constexpr void _reinit(Args &&...args) noexcept(detail::_nothrow_makeable) + requires has_type && detail::_makeable + && (detail::_nothrow_makeable || detail::_nothrow_makeable) { - if constexpr (detail::_nothrow_initializable) { + if constexpr (detail::_nothrow_makeable) { ::std::destroy_at(this); ::std::construct_at(this, ::std::in_place_type, FWD(args)...); } else { T tmp{FWD(args)...}; // may throw, and the storage is untouched until it cannot ::std::destroy_at(this); - ::std::construct_at(this, ::std::in_place_type, ::std::move(tmp)); + ::std::construct_at(this, ::std::in_place_type, ::std::move(tmp)); // cannot throw: see above } } @@ -410,10 +432,8 @@ struct sum { // The alternative that changes is CONSTRUCTED, never assigned to: assignment asks nothing of `Ts` // beyond what construction already asks, so a type with no assignment operator at all is still // assignable through the sum. - constexpr sum &operator=(sum const &other) noexcept((... && ::std::is_nothrow_copy_constructible_v)) - requires(... - && (::std::is_copy_constructible_v - && (::std::is_nothrow_copy_constructible_v || ::std::is_nothrow_move_constructible_v))) + constexpr sum &operator=(sum const &other) noexcept(_nothrow_copyable) + requires _copy_assignable { if (this != &other) { detail::invoke_type_variadic_union( // @@ -432,7 +452,7 @@ struct sum { // Moving is offered only where it cannot throw, for the reason given on _reinit - and where it can, // a nothrow-copyable sum is still assignable from an rvalue, by copy. constexpr sum &operator=(sum &&other) noexcept - requires(... && ::std::is_nothrow_move_constructible_v) + requires _nothrow_movable { if (this != &other) { detail::invoke_type_variadic_union( // @@ -733,6 +753,17 @@ struct sum { template explicit sum(::std::in_place_type_t, auto &&...) -> sum; template explicit sum(T) -> sum<::std::remove_cvref_t>; +namespace detail { +// The value lift builds `sum>` - a class whose body refuses an in_place tag as +// an alternative. MSVC (C++20 mode) compiles a candidate's noexcept-specifier once deduction +// succeeds, BEFORE the constraint rejects the tag, so the specifier must not name that sum unless +// the guard holds: a guarded specialization, as `_nothrow_eq_with` is, and for the same reason. +template constexpr inline bool _nothrow_sum_lift = false; +template + requires(not some_in_place_type) +constexpr inline bool _nothrow_sum_lift = ::std::is_nothrow_constructible_v>, Src>; +} // namespace detail + // Lifts /** * @brief TODO @@ -741,7 +772,7 @@ template explicit sum(T) -> sum<::std::remove_cvref_t>; * @return TODO */ [[nodiscard]] constexpr auto as_sum(auto &&src) // - noexcept(detail::_nothrow_initializable<::std::remove_cvref_t, decltype(src)>) -> decltype(auto) + noexcept(detail::_nothrow_sum_lift) -> decltype(auto) requires(not some_in_place_type) { return sum<::std::remove_cvref_t>(FWD(src)); @@ -755,8 +786,8 @@ template explicit sum(T) -> sum<::std::remove_cvref_t>; */ template [[nodiscard]] constexpr auto as_sum(::std::in_place_type_t, auto &&...args) // - noexcept(detail::_nothrow_initializable) -> decltype(auto) - requires detail::_initializable + noexcept(::std::is_nothrow_constructible_v, ::std::in_place_type_t, decltype(args)...>) -> decltype(auto) + requires ::std::is_constructible_v, ::std::in_place_type_t, decltype(args)...> { return sum(::std::in_place_type, FWD(args)...); } diff --git a/tests/fn/choice.cpp b/tests/fn/choice.cpp index 47a642dc..58e8ac83 100644 --- a/tests/fn/choice.cpp +++ b/tests/fn/choice.cpp @@ -35,6 +35,25 @@ struct NonCopyable final { template concept can_in_place = requires(Args... args) { S{std::in_place_type, args...}; }; +// Every special member of choice is defaulted, and choice adds no state to sum - so each must behave +// exactly as sum's, down to its noexcept and its constraints. +template consteval bool special_members_follow_sum() +{ + using C = fn::choice; + using S = fn::sum; + return std::is_copy_constructible_v == std::is_copy_constructible_v // + && std::is_nothrow_copy_constructible_v == std::is_nothrow_copy_constructible_v // + && std::is_move_constructible_v == std::is_move_constructible_v // + && std::is_nothrow_move_constructible_v == std::is_nothrow_move_constructible_v // + && std::is_copy_assignable_v == std::is_copy_assignable_v // + && std::is_nothrow_copy_assignable_v == std::is_nothrow_copy_assignable_v // + && std::is_move_assignable_v == std::is_move_assignable_v // + && std::is_nothrow_move_assignable_v == std::is_nothrow_move_assignable_v // + && std::is_destructible_v == std::is_destructible_v // + && std::is_nothrow_destructible_v == std::is_nothrow_destructible_v // + && std::is_trivially_destructible_v == std::is_trivially_destructible_v; +} + } // anonymous namespace TEST_CASE("choice non-monadic functionality", "[choice]") @@ -821,3 +840,46 @@ TEST_CASE("choice assignment", "[choice][assignment]") SUCCEED(); } } + +TEST_CASE("choice special members", "[choice]") +{ + using fn::choice; + + // choice declares all five, and defaults all five: the copy constructor because a user-declared + // move constructor would otherwise delete it, and the two assignments because they would otherwise + // be deleted and suppressed in turn. Removing a `= default`, adding a `noexcept` the base does not + // promise, or narrowing a requires-clause would all break the equalities below. + static_assert(special_members_follow_sum()); + static_assert(special_members_follow_sum()); + static_assert(special_members_follow_sum()); + static_assert(special_members_follow_sum()); + static_assert(special_members_follow_sum()); + + // ... and what they follow it TO, so that the equalities cannot be satisfied by both being wrong + static_assert(std::is_nothrow_copy_constructible_v>); + static_assert(std::is_nothrow_move_constructible_v>); + static_assert(std::is_nothrow_copy_assignable_v>); + static_assert(std::is_nothrow_move_assignable_v>); + static_assert(std::is_nothrow_destructible_v>); + static_assert(not std::is_trivially_destructible_v>); // the base destroys its alternative + + // a throwing copy is reported as one, rather than promised away + static_assert(std::is_copy_constructible_v>); + static_assert(not std::is_nothrow_copy_constructible_v>); + static_assert(std::is_nothrow_move_constructible_v>); + static_assert(not std::is_nothrow_copy_assignable_v>); + static_assert(std::is_nothrow_move_assignable_v>); + + // an alternative that cannot be copied takes copy construction and copy assignment with it + static_assert(not std::is_copy_constructible_v>); + static_assert(not std::is_copy_assignable_v>); + static_assert(std::is_nothrow_move_constructible_v>); + static_assert(std::is_nothrow_move_assignable_v>); + + // ... and one that can be neither copied nor moved leaves none of the four + static_assert(not std::is_copy_constructible_v>); + static_assert(not std::is_move_constructible_v>); + static_assert(not std::is_copy_assignable_v>); + static_assert(not std::is_move_assignable_v>); + SUCCEED(); +} diff --git a/tests/fn/pack.cpp b/tests/fn/pack.cpp index c6267aa9..bfa60554 100644 --- a/tests/fn/pack.cpp +++ b/tests/fn/pack.cpp @@ -13,6 +13,9 @@ #include #include +#include +#include +#include #include #include @@ -34,8 +37,89 @@ concept can_append_in_place = requires(V v, Args... args) { FWD(v).append(std::i template concept can_append = requires(V v, Arg arg) { FWD(v).append(FWD(arg)); }; +template +concept can_as_pack = requires(Args &&...args) { fn::as_pack(FWD(args)...); }; + +// pack declares no special member, so each one is implicit - composed from the _element bases which +// hold the data. Ask the elements, not the element types: a holder of a reference or of a const +// member answers assignment differently than the bare type would. +template using elem = fn::detail::_element<0, T>; +template consteval bool special_members_follow_elements() +{ + using P = fn::pack; + bool ok = std::is_copy_constructible_v

== (... && std::is_copy_constructible_v>); + ok = ok && std::is_nothrow_copy_constructible_v

== (... && std::is_nothrow_copy_constructible_v>); + ok = ok && std::is_move_constructible_v

== (... && std::is_move_constructible_v>); + ok = ok && std::is_nothrow_move_constructible_v

== (... && std::is_nothrow_move_constructible_v>); + ok = ok && std::is_copy_assignable_v

== (... && std::is_copy_assignable_v>); + ok = ok && std::is_nothrow_copy_assignable_v

== (... && std::is_nothrow_copy_assignable_v>); + ok = ok && std::is_move_assignable_v

== (... && std::is_move_assignable_v>); + ok = ok && std::is_nothrow_move_assignable_v

== (... && std::is_nothrow_move_assignable_v>); + ok = ok && std::is_destructible_v

== (... && std::is_destructible_v>); + ok = ok && std::is_nothrow_destructible_v

== (... && std::is_nothrow_destructible_v>); + ok = ok && std::is_trivially_destructible_v

== (... && std::is_trivially_destructible_v>); + return ok; +} + } // namespace +// pack is an aggregate at every layer - the element holder, the implementation, and pack itself. +// Everything above rests on that: brace initialization with elision, the structural type, and +// special members composed from the elements' own. A constructor added anywhere in the stack would +// change all of it in silence; these assertions fail instead. +TEST_CASE("design: an aggregate, at every layer", "[pack][design]") +{ + using fn::pack; + + WHEN("aggregates") + { + static_assert(std::is_aggregate_v>); + static_assert(std::is_aggregate_v>); + static_assert(std::is_aggregate_v, int, int &>>); + static_assert(std::is_aggregate_v>); + static_assert(std::is_aggregate_v>); + SUCCEED(); + } + + WHEN("brace elision") + { + // flat initializers reach the elements through two layers of subaggregate without written braces + constexpr pack p{3, 0.5}; + static_assert(fn::get<0>(p) == 3); + CHECK(fn::get<1>(p) == 0.5); + } + + WHEN("special members follow the elements") + { + static_assert(special_members_follow_elements<>()); + static_assert(special_members_follow_elements()); + static_assert(special_members_follow_elements()); + static_assert(special_members_follow_elements()); + static_assert(special_members_follow_elements>()); + static_assert(special_members_follow_elements()); + static_assert(special_members_follow_elements()); + static_assert(special_members_follow_elements, int &>()); + + struct Immovable { + Immovable(Immovable &&) = delete; + }; + static_assert(special_members_follow_elements()); + + // anchors, so the equalities above cannot be satisfied by both sides being wrong at once + static_assert(std::is_nothrow_move_constructible_v>); + static_assert(not std::is_nothrow_copy_constructible_v>); + static_assert(not std::is_copy_constructible_v>>); + static_assert(std::is_nothrow_move_constructible_v>>); + static_assert(not std::is_move_constructible_v>); + static_assert(std::is_copy_constructible_v>); + static_assert(not std::is_copy_assignable_v>); + static_assert(not std::is_copy_assignable_v>); + static_assert(std::is_trivially_destructible_v>); + static_assert(not std::is_trivially_destructible_v>); + SUCCEED(); + } +} + TEST_CASE("pack", "[pack]") { using fn::pack; @@ -209,6 +293,19 @@ TEST_CASE("append value categories", "[pack][append]") CHECK(std::move(std::as_const(s)).append(std::in_place_type).invoke_r(check) == 1); CHECK(std::move(s).append(std::in_place_type).invoke_r(check) == 1); } + + WHEN("aggregate forwarding") + { + // braces elide through the aggregate: three ints construct the array element in place + auto q = s.append(std::in_place_type>, 5, 6, 7); + static_assert(std::same_as>>); + CHECK(q.invoke([](int, std::string_view, A, std::array const &a) // + { return a[0] * 100 + a[1] * 10 + a[2]; }) + == 567); + static_assert(fn::pack<>{}.append(std::in_place_type>, 5, 6, 7).invoke([](auto const &a) { + return a[0] * 100 + a[1] * 10 + a[2]; + }) == 567); + } } WHEN("deduced type") @@ -304,6 +401,22 @@ TEST_CASE("append value categories", "[pack][append]") static_assert(not can_append_in_place, fn::sum>); static_assert(not can_append_in_place, fn::sum_for>); + // relocation is part of the question: the elements already held move into the new pack in the + // pack's own value category, and an element which cannot make that move rejects the append + // instead of hard-erroring inside it + static_assert(can_append_in_place> &&, int, int>); + static_assert(not can_append_in_place> &, int, int>); + static_assert(not can_append_in_place> const &, int, int>); + static_assert(not can_append> const &, int>); + // the same guard covers both operands when appending a whole pack + static_assert(can_append &, pack>>); + static_assert(not can_append &, pack> const &>); + static_assert(not can_append> const &, pack>); + // const propagates through a reference element, so a const pack cannot rebind it into the new + // pack's non-const reference element - the same pack, non-const, can + static_assert(can_append_in_place &, int, int>); + static_assert(not can_append_in_place const &, int, int>); + SUCCEED(); } } @@ -322,6 +435,11 @@ TEST_CASE("pack noexcept", "[pack][noexcept]") Quiet(Quiet const &) noexcept {} Quiet(Quiet &&) noexcept {} }; + struct Evil { + Evil() = default; + explicit Evil(Evil &&) noexcept {} + Evil(Evil const &) { throw std::runtime_error{"copied"}; } + }; WHEN("append") { @@ -334,9 +452,35 @@ TEST_CASE("pack noexcept", "[pack][noexcept]") // ... and so can relocating an element the pack already holds, even where the new one cannot static_assert(not noexcept(std::declval &>().append(std::in_place_type, 1))); static_assert(not noexcept(std::declval &>().append(1))); // deduced form + + // relocation copy-initializes ([dcl.init.aggr]/4.3), and copy-initialization cannot reach an + // explicit constructor - a promise computed from is_nothrow_constructible_v would see the + // explicit nothrow move and promise what the deed cannot keep: this throw must propagate + static_assert(std::is_nothrow_constructible_v); // the questions disagree here ... + static_assert(not noexcept(std::declval &&>().append(std::in_place_type, 1))); // ... deed answers + pack p{Evil{}}; // elided, nothing copied yet + CHECK_THROWS_AS(std::move(p).append(std::in_place_type, 1), std::runtime_error); SUCCEED(); } + WHEN("as_pack") + { + // asked of the braces as_pack performs - a question about pack's (nonexistent) constructors + // would answer false for every argument list + static_assert(noexcept(fn::as_pack(true, 12))); + static_assert(std::same_as>); + static_assert(not noexcept(fn::as_pack(Throwy{}))); // moving the argument in can throw ... + Throwy t{}; + static_assert(noexcept(fn::as_pack(t))); // ... but an lvalue binds, and binding cannot + struct NoMove { + NoMove() = default; + NoMove(NoMove &&) = delete; + }; + static_assert(not can_as_pack); // constrained on the same initialization ... + static_assert(can_as_pack); // ... which a binding reference element satisfies + CHECK(fn::as_pack(t, 12).invoke([&t](Throwy const &x, int i) { return &x == &t && i == 12; })); + } + WHEN("invoke") { constexpr auto nothrow_fn = [](auto &&...) noexcept { return 0; }; @@ -345,7 +489,21 @@ TEST_CASE("pack noexcept", "[pack][noexcept]") static_assert(not noexcept(std::declval &>().invoke(throwing_fn))); static_assert(noexcept(std::declval &>().template invoke_r(nothrow_fn))); static_assert(not noexcept(std::declval &>().template invoke_r(throwing_fn))); - SUCCEED(); + + // Ret is entered by INVOKE's implicit conversion; the promise asks that call rather than + // is_nothrow_constructible_v, whose direct-initialization would reach the explicit move + static_assert(std::is_nothrow_constructible_v); + pack p{1}; + Evil ev{}; + auto give_evil = [&ev](int) noexcept -> Evil && { return std::move(ev); }; + static_assert(not noexcept(std::declval &>().template invoke_r(give_evil))); + CHECK_THROWS_AS(p.invoke_r(give_evil), std::runtime_error); + // exactly std::invoke_r's own promise - conservative for a prvalue result (the deed elides, + // and indeed nothing throws below), but never a lie + auto make_evil = [](int) noexcept -> Evil { return {}; }; + static_assert(noexcept(std::declval &>().template invoke_r(make_evil)) + == std::is_nothrow_invocable_r_v); + CHECK_NOTHROW(p.invoke_r(make_evil)); } } diff --git a/tests/fn/sum.cpp b/tests/fn/sum.cpp index f2988a75..70e8fd35 100644 --- a/tests/fn/sum.cpp +++ b/tests/fn/sum.cpp @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include namespace { struct TestType final { @@ -73,6 +75,106 @@ template concept can_as_sum_value = requires(T v) { fn::as_sum(FWD(v)); }; } // anonymous namespace +// A sum brace-initializes the alternative it stores. That is a DESIGN DIRECTION, not an +// implementation detail, and this TEST_CASE exists to fail if it is ever switched to the +// parenthesized initialization `std::variant` uses. Many other tests would fail with it, but +// incidentally - this one is here to say what was chosen, and why. +// +// What braces buy: +// * Aggregate forwarding, through brace elision: `sum{in_place_type>, 1, 2, 3}`. +// `std::variant` cannot express this, and neither can parenthesized initialization - P0960 +// admits aggregates but forbids brace elision. +// * Coherence with `sum` being a structural type. A structural type's alternatives must themselves +// be structural - public-member types, written with braces, exactly as one writes them as a +// template argument. Storing them any other way would put the two spellings at odds. +// * Narrowing is rejected outright rather than silently truncating. +// +// What braces cost: +// * An initializer-list constructor wins where `std::variant` would call the (count, value) one: +// `sum>{in_place_type>, 3, 0}` holds `{3, 0}`, where a variant would hold +// `{0, 0, 0}`. This divergence is the cost of giving `sum` a capability which `std::variant` lacks. +// +// And the consequence that actually bites, which is why the last section is here: every trait that +// constrains or specifies the construction of an alternative must ask the BRACE question +// (`fn::detail::_initializable` / `_nothrow_initializable`), never `std::is_[nothrow_]constructible`, +// which asks about parentheses. Where the two disagree, the parenthesized answer is a lie. +TEST_CASE("design: braces, not parentheses", "[sum][design]") +{ + using fn::sum; + + WHEN("aggregate forwarding") + { + using A = std::array; + static_assert(can_in_place, A, int, int, int>); // braces elide; parentheses cannot + static_assert(not std::is_constructible_v); // ... which is why the std trait misleads + static_assert(fn::detail::_initializable); // ... and this is the trait that does not + + sum a{std::in_place_type, 1, 2, 3}; + CHECK(a.invoke([](A const &v) { return v[0] * 100 + v[1] * 10 + v[2]; }) == 123); + static_assert(sum{std::in_place_type, 1, 2, 3}.invoke([](A const &v) { return v[2]; }) == 3); + } + + WHEN("narrowing") + { + static_assert(not can_in_place, int, double>); // braces reject it ... + static_assert(std::is_constructible_v); // ... where parentheses would truncate in silence + SUCCEED(); + } + + WHEN("initializer-list constructors win") + { + // the price of the above, and the same mechanism as the trap below: `std::variant` would hold + // three zeroes here, and a sum holds the two elements it was written with + using V = std::vector; + sum v{std::in_place_type, 3, 0}; + CHECK(v.invoke([](V const &x) { return x.size(); }) == 2); + CHECK(v.invoke([](V const &x) { return x[0]; }) == 3); + } + + WHEN("the traits must ask the brace question") + { + // Braced initialization considers initializer-list constructors FIRST, so the two questions can + // answer differently - and the parenthesized answer is not the one the storage acts on. + struct Listy final { + Listy(int) noexcept {} // parentheses select this ... + Listy(std::initializer_list) noexcept(false) {} // ... braces select this + }; + static_assert(std::is_nothrow_constructible_v); // the parenthesized question: nothrow + static_assert(not fn::detail::_nothrow_initializable); // the braced question: it can throw + static_assert(not noexcept(sum{std::in_place_type, 1})); // the sum promises what it does + } + + WHEN("... including where a compiler answers it differently") + { + // The divergence also reaches the copy and move constructors, and the assignment built on them. + // `Evil` converts to double only as an rvalue, so `Evil{lvalue}` copies while `Evil{rvalue}` + // selects the throwing initializer-list constructor - which is what [over.match.list]/1 requires, + // [dcl.init.list]/3.2's same-type carve-out being for AGGREGATES only, and Evil is not one. + // gcc and clang>=20 have it so; clang<=19, AppleClang and MSVC pick the move constructor instead. + // + // So these pin the INVARIANT rather than the answer: whatever a compiler makes of the braces is + // what the sum must promise. Reading the parenthesized answer instead - which is always "nothrow + // move" here - declared a `noexcept` that std::terminate had to keep, and offered an assignment + // that destroyed the alternative it held and then failed to replace it. + struct Tag final {}; + struct Evil final { + int v; + Evil(Tag, int i) noexcept : v(i) {} // NOLINT: constructs without touching either trap + operator double() && noexcept { return v; } + Evil(std::initializer_list) { throw std::runtime_error("init-list"); } + Evil(Evil &&o) noexcept : v(o.v) {} + Evil(Evil const &o) noexcept(false) : v(o.v) {} + }; + static_assert(std::is_nothrow_move_constructible_v); // the parenthesized question, everywhere + + static_assert(std::is_nothrow_move_constructible_v> == fn::detail::_nothrow_initializable); + static_assert(std::is_nothrow_copy_constructible_v> + == fn::detail::_nothrow_initializable); + static_assert(std::is_move_assignable_v> == fn::detail::_nothrow_initializable); + SUCCEED(); + } +} + TEST_CASE("sum basic functionality tests", "[sum]") { // NOTE This test looks very similar to test in choice.cpp - for good reason. @@ -1280,21 +1382,35 @@ TEST_CASE("sum assignment", "[sum][assignment]") // throw, it is made into a temporary first, and only the (nothrow) move touches the storage. struct ThrowingCopy final { int v; - ThrowingCopy(int i) noexcept : v(i) {} // NOLINT: implicit on purpose - ThrowingCopy(ThrowingCopy const &o) : v(o.v) + constexpr ThrowingCopy(int i) noexcept : v(i) {} // NOLINT: implicit on purpose + constexpr ThrowingCopy(ThrowingCopy const &o) : v(o.v) { if (v == 0) throw std::runtime_error("copy"); } - ThrowingCopy(ThrowingCopy &&o) noexcept : v(o.v) {} + constexpr ThrowingCopy(ThrowingCopy &&o) noexcept : v(o.v) {} }; static_assert(std::is_copy_assignable_v>); static_assert(not std::is_nothrow_copy_assignable_v>); + constexpr auto value = [](ThrowingCopy const &t) { return t.v; }; sum a{ThrowingCopy{7}}; sum const bad{ThrowingCopy{0}}; CHECK_THROWS_AS(a = bad, std::runtime_error); - CHECK(a.invoke([](ThrowingCopy const &t) { return t.v; }) == 7); // untouched + CHECK(a.invoke(value) == 7); // untouched + + // the same arm, completing: the copy into the temporary succeeds, and only then is the old + // alternative destroyed and the temporary moved into the storage + sum const good{ThrowingCopy{5}}; + a = good; + CHECK(a.invoke(value) == 5); + + static_assert([] { + sum a{ThrowingCopy{7}}; + sum const good{ThrowingCopy{5}}; + a = good; // the temporary arm, in a constant expression + return a.invoke([](ThrowingCopy const &t) { return t.v; }) == 5; + }()); WHEN("the two arms in one sum") {