pack::append(std::in_place_type<T>) means two completely different things depending on a property
of T — and the difference is silent.
- If
T is default-constructible, the in-place overload (pack.hpp:48) wins and appends a
default-constructed T. This is the intent, and tests/fn/pack.cpp's WHEN("default constructor") covers it.
- If
T is not default-constructible, that overload's is_constructible_v<T, decltype(args)...>
conjunct fails with an empty pack, so it drops out — and the deduced-argument overload
(pack.hpp:108) picks the call up instead, appending the in_place_type_t<T> tag itself as a
pack element.
The deduced-argument append(Arg &&arg) overloads have no guard excluding the tag type. as_pack
does have one (pack.hpp:321: requires(not some_in_place_type<T>)) — the some_in_place_type
concept exists for exactly this — but append never got it.
Verified
g++ 16.1.1 and clang 22.1.6, C++20 — both accept all of this:
struct NoDefault final { int v; explicit NoDefault(int i) noexcept : v(i) {} };
struct HasDefault final { int v = 30; };
// intended: the in_place overload constructs the element
using Good = decltype(std::declval<fn::pack<int> &>().append(std::in_place_type<HasDefault>));
static_assert(std::same_as<Good, fn::pack<int, HasDefault>>);
// footgun: the tag is appended AS AN ELEMENT
using Bad = decltype(std::declval<fn::pack<int> &>().append(std::in_place_type<NoDefault>));
static_assert(std::same_as<Bad, fn::pack<int, std::in_place_type_t<NoDefault> const &>>);
It is not merely well-formed in an unevaluated context — the pack is really built, and the tag is
passed through to the callback:
static_assert([] {
fn::pack<int> p{7};
auto q = p.append(std::in_place_type<NoDefault>);
return q.size == 2 && q.invoke([](int i, auto &&tag) {
return i == 7 && std::same_as<std::remove_cvref_t<decltype(tag)>, std::in_place_type_t<NoDefault>>;
});
}());
So a user who writes p.append(std::in_place_type<T>) intending to append a default-constructed T,
against a T that has no default constructor, gets no diagnostic at all — just a pack with a tag
where the value should be, and a confusing error much later (or none).
Fix
Mirror as_pack's guard on the four deduced-argument append overloads (pack.hpp:108/122/136/150):
template <typename Arg>
requires(not some_in_place_type<::std::remove_cvref_t<Arg>>)
[[nodiscard]] constexpr auto append(Arg &&arg) & noexcept -> append_type<Arg>
The in-place overload is then the only candidate for a tag argument, and a non-default-constructible
T produces a clean "no viable overload" error instead of silently changing meaning.
Found while adding requires-clause coverage to tests/fn/pack.cpp (#85). Related: #282 (also
append, also decided outside the requires-clause).
pack::append(std::in_place_type<T>)means two completely different things depending on a propertyof
T— and the difference is silent.Tis default-constructible, the in-place overload (pack.hpp:48) wins and appends adefault-constructed
T. This is the intent, andtests/fn/pack.cpp'sWHEN("default constructor")covers it.Tis not default-constructible, that overload'sis_constructible_v<T, decltype(args)...>conjunct fails with an empty pack, so it drops out — and the deduced-argument overload
(
pack.hpp:108) picks the call up instead, appending thein_place_type_t<T>tag itself as apack element.
The deduced-argument
append(Arg &&arg)overloads have no guard excluding the tag type.as_packdoes have one (
pack.hpp:321:requires(not some_in_place_type<T>)) — thesome_in_place_typeconcept exists for exactly this — but
appendnever got it.Verified
g++ 16.1.1 and clang 22.1.6, C++20 — both accept all of this:
It is not merely well-formed in an unevaluated context — the pack is really built, and the tag is
passed through to the callback:
So a user who writes
p.append(std::in_place_type<T>)intending to append a default-constructedT,against a
Tthat has no default constructor, gets no diagnostic at all — just a pack with a tagwhere the value should be, and a confusing error much later (or none).
Fix
Mirror
as_pack's guard on the four deduced-argumentappendoverloads (pack.hpp:108/122/136/150):The in-place overload is then the only candidate for a tag argument, and a non-default-constructible
Tproduces a clean "no viable overload" error instead of silently changing meaning.Found while adding
requires-clause coverage totests/fn/pack.cpp(#85). Related: #282 (alsoappend, also decided outside the requires-clause).