Skip to content

pack: append(in_place_type<T>) silently appends the tag as an element when T is not default-constructible #283

Description

@Bronek

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrelease-0.1Planned for release 0.1

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions