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]
Symptom
Appending an lvalue to a
packdeduces a reference element (aspack's CTAD does), and the resulting call does not compile on gcc: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 asT{FWD(args)...}, and for a referenceTgcc 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: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 referenceT"has the same effect as direct-initializing an invented variabletof typeTfrom 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:
Standalone, no libfn. Same diagnostic in
-std=c++17,-std=c++20and-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:
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
appendoverload set; the constraint that decides which element types are viable isfn::detail::_initializable, whose reference leg exists for this same reason).Assisted-by: Claude:claude-opus-4-8[1m]