diff --git a/include/fn/choice.hpp b/include/fn/choice.hpp index e1b4d1cd..fed67d39 100644 --- a/include/fn/choice.hpp +++ b/include/fn/choice.hpp @@ -111,7 +111,7 @@ struct choice : sum { */ template constexpr explicit choice(::std::in_place_type_t d, auto &&...args) noexcept - requires has_type + requires has_type && detail::_initializable : _impl(d, FWD(args)...) { } diff --git a/include/fn/detail/pack_impl.hpp b/include/fn/detail/pack_impl.hpp index a5818d6f..4c0ee5c7 100644 --- a/include/fn/detail/pack_impl.hpp +++ b/include/fn/detail/pack_impl.hpp @@ -21,6 +21,20 @@ template <::std::size_t I, typename T> struct _element { T v; // NOSONAR cpp:S6226 MSVC ignores the attribute }; +// Initializes one element. A reference element is spelled as a cast rather than `T{arg}`: the two +// are the same thing ([expr.type.conv]/1.3 defines the latter as direct-initializing an invented +// variable of type T, and [dcl.init.list]/3.9 initializes a reference from a single element whose +// 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 +{ + if constexpr (::std::is_reference_v) + return T(FWD(args)...); // a single argument, so this is a cast expression: it binds + else + return T{FWD(args)...}; +} + template struct pack_impl; template struct _pack_append; @@ -86,9 +100,10 @@ struct pack_impl<::std::index_sequence, Ts...> : _element... { template static constexpr auto _append(Self &&self, auto &&...args) noexcept // -> pack_impl<::std::index_sequence, Ts..., T> - requires(not _some_sum) && (not _some_pack) && ::std::is_constructible_v + requires(not _some_sum) && (not _some_pack) && _initializable { - return {static_cast>(FWD(self)._element::v)..., T{FWD(args)...}}; + return {static_cast>(FWD(self)._element::v)..., + _make_element(FWD(args)...)}; } template diff --git a/include/fn/detail/traits.hpp b/include/fn/detail/traits.hpp index e0da82dc..4e24c06e 100644 --- a/include/fn/detail/traits.hpp +++ b/include/fn/detail/traits.hpp @@ -7,9 +7,21 @@ #define INCLUDE_FN_DETAIL_TRAITS #include +#include namespace fn::detail { +// The storage initializes an element as `T{args...}`, so a constraint on it must ask the same +// question: `is_constructible_v` spells parenthesized initialization, which for an aggregate +// performs no brace elision (`std::array` is not "constructible" from 3 ints) and permits +// narrowing where brace initialization rejects it. A reference element is not brace-initialized but +// bound, and `T{...}` for a reference `T` is not even a portable question to ask (gcc rejects it, +// clang accepts) - so that leg asks about the binding instead. +template +concept _initializable // + = (::std::is_reference_v && ::std::is_constructible_v) // + || ((not ::std::is_reference_v) && requires { T{::std::declval()...}; }); + // Change any rvalue or empty value to prvalue, but leave lvalues unchanged. // This is meant to find the type of data members which won't bind to rvalues. template extern T _as_value; diff --git a/include/fn/pack.hpp b/include/fn/pack.hpp index 5eea57c4..3189ca02 100644 --- a/include/fn/pack.hpp +++ b/include/fn/pack.hpp @@ -46,7 +46,7 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) & noexcept -> append_type - requires ::std::is_constructible_v + requires detail::_initializable && requires { append_type{_impl::template _append(*this, FWD(args)...)}; } { return {_impl::template _append(*this, FWD(args)...)}; @@ -61,7 +61,7 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) const & noexcept -> append_type - requires ::std::is_constructible_v + requires detail::_initializable && requires { append_type{_impl::template _append(*this, FWD(args)...)}; } { return {_impl::template _append(*this, FWD(args)...)}; @@ -76,7 +76,7 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) && noexcept -> append_type - requires ::std::is_constructible_v + requires detail::_initializable && requires { append_type{_impl::template _append(::std::move(*this), FWD(args)...)}; } { return {_impl::template _append(::std::move(*this), FWD(args)...)}; @@ -91,7 +91,7 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) const && noexcept -> append_type - requires ::std::is_constructible_v + requires detail::_initializable && requires { append_type{_impl::template _append(::std::move(*this), FWD(args)...)}; } { return {_impl::template _append(::std::move(*this), FWD(args)...)}; diff --git a/include/fn/sum.hpp b/include/fn/sum.hpp index 4ba80034..5d8d781c 100644 --- a/include/fn/sum.hpp +++ b/include/fn/sum.hpp @@ -244,7 +244,7 @@ struct sum { */ template constexpr explicit sum(::std::in_place_type_t, auto &&...args) - requires has_type + requires has_type && detail::_initializable : data(detail::make_variadic_union(FWD(args)...)), index(detail::type_index) { } @@ -616,6 +616,7 @@ template explicit sum(T) -> sum<::std::remove_cvref_t>; * @return TODO */ [[nodiscard]] constexpr auto as_sum(auto &&src) -> decltype(auto) + requires(not some_in_place_type) { return sum<::std::remove_cvref_t>(FWD(src)); } @@ -626,7 +627,9 @@ template explicit sum(T) -> sum<::std::remove_cvref_t>; * @param src TODO * @return TODO */ -template [[nodiscard]] constexpr auto as_sum(::std::in_place_type_t, auto &&...args) -> decltype(auto) +template +[[nodiscard]] constexpr auto as_sum(::std::in_place_type_t, auto &&...args) -> decltype(auto) + requires detail::_initializable { return sum(::std::in_place_type, FWD(args)...); } diff --git a/tests/fn/choice.cpp b/tests/fn/choice.cpp index b7202207..fbb8896b 100644 --- a/tests/fn/choice.cpp +++ b/tests/fn/choice.cpp @@ -30,6 +30,9 @@ struct NonCopyable final { NonCopyable &operator=(NonCopyable const &) = delete; }; +template +concept can_in_place = requires(Args... args) { S{std::in_place_type, args...}; }; + } // anonymous namespace TEST_CASE("choice non-monadic functionality", "[choice]") @@ -342,6 +345,17 @@ TEST_CASE("choice non-monadic functionality", "[choice]") auto b = choice{std::in_place_type, 42}; static_assert(std::is_same_v>); } + + WHEN("constraints") + { + static_assert(can_in_place, NonCopyable, int>); + static_assert(not can_in_place, int, int>); // int is not an alternative + + // the constructor forwards to sum's, and rejects what sum rejects + static_assert(not can_in_place, NonCopyable>); // no default ctor + static_assert(not can_in_place, NonCopyable, char const *>); // not constructible from + SUCCEED(); + } } WHEN("forwarding constructors (aggregate)") diff --git a/tests/fn/pack.cpp b/tests/fn/pack.cpp index c508e89e..7ba0f5c7 100644 --- a/tests/fn/pack.cpp +++ b/tests/fn/pack.cpp @@ -12,6 +12,7 @@ #include +#include #include #include @@ -228,6 +229,37 @@ TEST_CASE("append value categories", "[pack][append]") CHECK(std::move(s).append(B{30}).invoke(check)); } + WHEN("reference element") + { + // Evaluated, not merely decltype'd: a reference element must BIND to the argument, and the + // element-initializing expression is only instantiated when the call is actually made + C c{}; + auto q = s.append(std::in_place_type, c); + static_assert(std::same_as>); + CHECK(q.invoke([&c](int, std::string_view, A, B &b) { return &b == static_cast(&c); })); + + auto r = s.append(c); // deduced, so the element type is C & + static_assert(std::same_as>); + CHECK(r.invoke([&c](int, std::string_view, A, C &x) { return &x == &c; })); + + c.v = 77; // the same object, observed through both packs + CHECK(q.invoke([](int, std::string_view, A, B &b) { return b.v == 77; })); + CHECK(r.invoke([](int, std::string_view, A, C &x) { return x.v == 77; })); + + WHEN("constexpr") + { + static_assert([] { + fn::pack p{1}; + B b{5, 6}; + auto q = p.append(std::in_place_type, b); + auto r = p.append(b); + b.v = 9; + return q.invoke([](int, B &x) { return x.v == 9; }) && r.invoke([](int, B &x) { return x.v == 9; }); + }()); + SUCCEED(); + } + } + WHEN("pack on the right side, deduced") { constexpr fn::pack a{true, 3, B{14}}; @@ -251,6 +283,12 @@ TEST_CASE("append value categories", "[pack][append]") static_assert(can_append_in_place); static_assert(not can_append_in_place); // B is not constructible from it + // the element is brace-initialized, so an aggregate is appended element-wise, exactly as `sum` + // constructs one - a constraint spelled with is_constructible_v would reject this + static_assert(can_append_in_place, int, int, int>); + static_assert(not can_append_in_place, int, int, int, int>); // one too many + static_assert(not can_append_in_place); // narrowing + // in_place_type selects the element type, it is never itself an element: with no arguments and // no default constructor there is nothing to construct, and the deduced-Arg overload must not // pick the call up and append the tag instead diff --git a/tests/fn/sum.cpp b/tests/fn/sum.cpp index e7ff715a..66e3bc75 100644 --- a/tests/fn/sum.cpp +++ b/tests/fn/sum.cpp @@ -39,6 +39,15 @@ template auto read_nttp() { return S.invoke([](auto const &...args) { return (0.0 + ... + static_cast(args)); }); } + +template +concept can_in_place = requires(Args... args) { S{std::in_place_type, args...}; }; + +template +concept can_as_sum = requires(Args... args) { fn::as_sum(std::in_place_type, args...); }; + +template +concept can_as_sum_value = requires(T v) { fn::as_sum(FWD(v)); }; } // anonymous namespace TEST_CASE("sum basic functionality tests", "[sum]") @@ -63,6 +72,20 @@ TEST_CASE("sum basic functionality tests", "[sum]") constexpr auto b = fn::as_sum(std::in_place_type, 12); static_assert(std::same_as const>); static_assert(b == fn::sum{12l}); + + WHEN("constraints") + { + static_assert(can_as_sum); + static_assert(can_as_sum, int, int, int>); // an aggregate, brace-initialized + static_assert(not can_as_sum); // no default constructor + static_assert(not can_as_sum); // not constructible from it + + // the tag selects the alternative, it is never itself one: with nothing to construct there is + // no viable lift at all, rather than a sum whose alternative is the tag + static_assert(not can_as_sum_value const &>); + static_assert(can_as_sum_value); + SUCCEED(); + } } WHEN("sum_for") @@ -207,10 +230,33 @@ TEST_CASE("sum basic functionality tests", "[sum]") auto b = sum{std::in_place_type, 42}; static_assert(std::is_same_v>); } + + WHEN("constraints") + { + static_assert(can_in_place, NonCopyable, int>); + static_assert(not can_in_place, int, int>); // int is not an alternative + + // an argument list the alternative cannot be constructed from is not viable - viability must + // answer here, not fail to compile inside variadic_union, beyond SFINAE's reach + static_assert(not can_in_place, NonCopyable>); // no default ctor + static_assert(not can_in_place, NonCopyable, char const *>); // not constructible from + SUCCEED(); + } } WHEN("forwarding constructors (aggregate)") { + WHEN("constraints") + { + using T = std::array; + // the element is brace-initialized, which elides braces for an aggregate - a constraint + // spelled with is_constructible_v (parenthesized init) would reject this very construction + static_assert(can_in_place, T, int, int, int>); + static_assert(not can_in_place, T, int, int, int, int>); // one too many + static_assert(not can_in_place, int, double>); // narrowing, rejected by braces + SUCCEED(); + } + WHEN("regular") { sum> a{std::in_place_type>, 1, 2, 3};