https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94062
--- Comment #13 from Jonathan Wakely <redi at gcc dot gnu.org> --- (In reply to m.cencora from comment #12) > So unless I am missing something, I see no escape hatch for making such > constructor ill-formed for some types. Consider: #include <tuple> struct X { template<typename T> X(T) { static_assert(!std::is_same_v<T,int>); } }; static_assert(std::is_constructible_v<X, int>); static_assert(std::is_constructible_v<std::tuple<X>, int>); std::tuple<X> t{1}; The fact that a type meets a function's constraints does not mean it has to compile, it just means the function is not removed from the candidate functions for overload resolution. The standard is pretty clear about that: Constraints: the conditions for the function’s participation in overload resolution That's it. It doesn't mean any more than that. The constructor's effects say it initializes the element from the argument, and if that initialization is ill-formed (e.g. because it requires a copy that isn't elided) then the constructor is ill-formed. > > That would mean that std::tuple<Bar> changes layout if you later add a move > > constructor to Bar. > > Not sure how, because you can create std::tuple<Bar> already by using other > constructors, e.g.: That's irrelevant. I'm talking about a change that would make it possible to construct non-movable types, so if you change whether a type is movable or not, it would affect std::tuple. The fact other constructors are already present is irrelevant, I'm not talking about making anything depend on those. Bar is an empty class, so we will try to make tuple<Bar> use a potentially-overlapping subobject (storing a Bar as a base class today, or a data member with [[no_unique_address] in the near future). That doesn't support initialization from ConvertibleToBar because the copy cannot be elided. I could change the tuple code to not use a potentially-overlapping subobject for non-movable types, so that copy elision works. But now if you add a move constructor the decision of whether to use a potentially-overlapping subobject changes. So that means the std::tuple layout would depend on the presence of a move constructor. #include <type_traits> namespace std { template<typename T, bool compress = std::is_empty_v<T> && std::is_move_constructible_v<T>> struct Tuple_impl { T t; template<typename U> Tuple_impl(U&& u) : t(static_cast<U&&>(u)) { } }; template<typename T> struct Tuple_impl<T, true> { [[no_unique_address]] T t; template<typename U> Tuple_impl(U&& u) : t(static_cast<U&&>(u)) { } }; template<typename... T> struct tuple : Tuple_impl<T>... { template<typename... U> tuple(U&&... u) : Tuple_impl<T>(static_cast<U&&>(u))... { } }; } struct Bar { Bar() = default; Bar(int i); Bar(const Bar&) = delete; #ifdef MAKE_IT_MOVABLE Bar(Bar&&) = default; #endif }; struct ConvertibleToBar { operator Bar(); }; std::tuple<Bar> fail1() { return {ConvertibleToBar{}}; } static_assert( sizeof(std::tuple<Bar, int>) == 8 ); If you define MAKE_IT_MOVABLE then the static assertion fails. This shows that the toy implementation above can support construction from ConvertibleToBar but at the cost of changing layout if a move constructor is added later. I don't think supporting guaranteed elision for non-copyable, non-movable, empty types is a sufficiently important use case to make the layout fragile.