[Bug c++/94062] Cannot construct tuple from convertible types

redi at gcc dot gnu.org gcc-bugzilla@gcc.gnu.org
Mon Aug 17 12:51:57 GMT 2020


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.


More information about the Gcc-bugs mailing list