https://gcc.gnu.org/bugzilla/show_bug.cgi?id=115285
--- Comment #10 from Jonathan Wakely <redi at gcc dot gnu.org> --- When deciding whether a key already exists in the hash set we use these overloads: template<typename _Kt> static __conditional_t< __and_<__is_nothrow_invocable<_Hash&, const key_type&>, __not_<__is_nothrow_invocable<_Hash&, _Kt>>>::value, key_type, _Kt&&> _S_forward_key(_Kt&& __k) { return std::forward<_Kt>(__k); } static const key_type& _S_forward_key(const key_type& __k) { return __k; } If the value being inserted, __k, can be hashed directly using the hash function then we return it unchanged, and then hash it. If it can't be hashed directly, we convert it to the container's key_type, which is TrimmedStr. In this case, a std::string can be hash directly without constructing a TrimmedStr (because of the questionable std::hash specialization using inheritance). So we don't convert it to TrimmedStr and just hash it directly. Hashing "foo"s and "foo "s give different hash values, so we do not consider them to be equivalent keys. While I think the code is highly questionable, the standard does say that inserting [first, last) is equivalent to insert(*first) for each iterator, and that would force an implicit conversion to value_type. So I'm not sure if the optimization to avoid temporaries (PR 96088) is actually allowed by the standard.