Skip to content

pack::append builds a reference element with T{arg}, which gcc rejects — appending an lvalue does not compile #287

Description

@Bronek

Symptom

Appending an lvalue to a pack deduces a reference element (as pack's CTAD does), and the resulting call does not compile on gcc:

fn::pack<int> p{1};
B b{30};

auto q1 = p.append(b);                          // pack<int, B &>
auto q2 = p.append(std::in_place_type<B &>, b); // pack<int, B &>
include/fn/detail/pack_impl.hpp:91: error: cannot bind non-const lvalue reference of type 'B&'
                                           to an rvalue of type 'B'
   91 |     return {static_cast<...>(FWD(self)._element<Is, Ts>::v)..., T{FWD(args)...}};

clang++ 22.1.6 accepts both; g++ 16.1.1 rejects both. A prvalue argument (p.append(B{7}), a non-reference element) compiles everywhere.

The construct is pack_impl.hpp:91 — the new element is built as T{FWD(args)...}, and for a reference T gcc reads the braced functional-cast form as materializing a temporary and then failing to bind.

Why it went unnoticed

Every existing test of a reference-element append only takes decltype(...) of the call:

static_assert(std::same_as<decltype(s.append(std::in_place_type<B &>, c2)), T::append_type<B &>>);

which never instantiates _append's body. The suite is green on gcc while the operation is unusable there.

Which compiler is right

clang. Per [expr.type.conv]/1.3, T{x} for a reference T "has the same effect as direct-initializing an invented variable t of type T from the initializer"; and [dcl.init.list]/3.9 — which precedes the temporary-generating /3.10 — says that when the initializer list "has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, the object or reference is initialized from that element", i.e. it binds. /3.10 ("if T is a reference type, a prvalue is generated") only applies when /3.9 does not.

gcc applies /3.10 to the functional-cast spelling, and contradicts itself in the process — the equivalent declaration is accepted:

using Ri = int &;
int i = 0;

Ri r1{i};        // g++: OK
auto r4 = Ri{i}; // g++: error: cannot bind non-const lvalue reference of type 'Ri' {aka 'int&'}
                 //        to an rvalue of type 'int'

Standalone, no libfn. Same diagnostic in -std=c++17, -std=c++20 and -std=c++23; clang++ accepts all forms. So this is a gcc conformance bug, and libfn must avoid the spelling it gets wrong.

Fix

Initialize a reference element as the binding it is, rather than through the braced functional cast:

template <typename T> [[nodiscard]] constexpr auto _make_element(auto &&...args) -> T
{
  if constexpr (::std::is_reference_v<T>)
    return T(FWD(args)...); // a single argument, so this is a cast expression: it binds
  else
    return T{FWD(args)...};
}

Both compilers then agree, and the tests gain an evaluated reference-element append (address identity, plus mutation observed through the pack) with a constexpr twin, so the body can no longer go untested.

Related: #283 and #284 (the same append overload set; the constraint that decides which element types are viable is fn::detail::_initializable, whose reference leg exists for this same reason).

Assisted-by: Claude:claude-opus-4-8[1m]

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