It seems like a std::tuple
containing one or more references has unexpected behavior with regards to construction and assignment (especially copy/move construction and copy/move assignment). It's different from the behavior of both std::reference_wrapper
(changes the referred to object) and a struct with a member reference variable (assignment operator deleted). It allows for the convenient std::tie
python like multiple return values, but it also allows obviously incorrect code like the following (link here):
#include <tuple>
int main()
{
std::tuple<int&> x{std::forward_as_tuple(9)}; // OK - doesn't seem like it should be
std::forward_as_tuple(5) = x; // OK - doesn't seem like it should be
// std::get<0>(std::forward_as_tuple(5)) = std::get<0>(x); // ERROR - and should be
return 0;
}
The standard seems to require or strongly hint at this behavior in the copy(ish) assignment section 20.4.2.2.9
of the latest working draft (Ti&
will collapse to lvalue ref):
template <class... UTypes> tuple& operator=(const tuple<UTypes...>& u);
9 Requires:
sizeof...(Types) == sizeof...(UTypes)
andis_assignable<Ti&, const Ui&>::value
is true for alli
.10 Effects: Assigns each element of u to the corresponding element of *this.
11 Returns: *this
Although the move(ish) construction section 20.4.2.1.20
is less clear (is_constructible<int&, int&&>
returns false
):
template <class... UTypes> EXPLICIT constexpr tuple(tuple<UTypes...>&& u);
18 Requires:
sizeof...(Types) == sizeof...(UTypes)
.19 Effects: For all
i
, the constructor initializes thei
th element of*this
withstd::forward<Ui>(get<i>(u))
.20 Remarks: This constructor shall not participate in overload resolution unless
is_constructible<Ti, Ui&&>::value
is true for alli
. The constructor isexplicit
if and only ifis_convertible<Ui&&, Ti>::value
is false for at least onei
.
These are not the only affected subsections.
The question is, why is this behavior desired? Also, if there are other parts of the standard at play, or I'm misunderstanding it, explain where I went wrong.
Thanks!
The constructor
Let's start with the constructor (your first line):
As we both know (just clarifying), decltype(std::forward_as_tuple(9))
is std::tuple<int&&>
.
Also note that:
std::is_constructible<int&, int&&>::value == false
on all compiler platforms. Therefore, according to the spec you quote, which is consistent with the latest C++1z working draft, the construction in your first line of code should not participate in overload resolution.
In clang with libc++ it correctly fails to compile according to this spec:
http://melpon.org/wandbox/permlink/7cTyS3luVn1XRXGv
With the latest gcc it incorrectly compiles according to this spec:
http://melpon.org/wandbox/permlink/CSoB1BTNF3emIuvm
And with the latest VS-2015 it incorrectly compiles according to this spec:
http://webcompiler.cloudapp.net
(this last link doesn't contain the correct code, you have to paste it in).
In your coliru link, although you are using clang, clang is implicitly using gcc's libstdc++ for the std::lib, so your results are consistent with mine.
From this survey, it appears that only libc++ is consistent both with the spec, and with your expectations. But that is not the end of the story.
The latest working draft spec that you have correctly quoted is different than the C++14 spec which looks like this:
template <class... UTypes> constexpr tuple(tuple<UTypes...>&& u);
Requires:
sizeof...(Types) == sizeof...(Types)
.is_constructible<Ti, Ui&&>::value
istrue
for all i.Effects: For all i, initializes the ith element of
*this
withstd::forward<Ui>(get<i>(u))
.Remark: This constructor shall not participate in overload resolution unless each type in
UTypes
is implicitly convertible to its corresponding type inTypes
.
It was N4387 that changed the specification after C++14.
In C++14 (and in C++11 as well), the onus is on the client to ensure that is_constructible<Ti, Ui&&>::value
is true
for all i. And if it is not, you have undefined behavior.
So your first line of code is undefined behavior in C++11 and C++14, and ill-formed in the C++1z working draft. With undefined behavior, anything can happen. So all of libc++, libstdc++ and VS are conforming according to the C++11 and C++14 specification.
Update inspired by T.C.'s comment below:
Note that the removal of this one constructor from overload resolution might allow the expression to use another constructor such as tuple(const tuple<UTypes...>& u)
. However this constructor is also removed from overload resolution by a similar Remarks clause.
std::is_constructible<int&, const int&>::value == false
Alas, T.C. may have found a defect in the working draft. The actual spec says:
std::is_constructible<int&, int&>::value == true
because the const
is applied to the reference, not the int
.
It appears that libstdc++ and VS have not yet implemented the post-C++14 spec, specified by N4387, and which pretty much specifies what your comments indicate should happen. libc++ has implemented N4387 for years, and served as a proof-of-implementation for that proposal.
The assignment
This requires:
std::is_assignable<int&, const int&>::value == true
and this is in fact true in your example assignment. In this line of code. However I think an argument could be made that it should instead require:
std::is_assignable<int&&, const int&>::value == true
which is false. Now if we only made this change, your assignment would be undefined behavior since this part of the spec is in a Requires clause. So if you really want to do this right, you need to move the spec into a Remarks clause with the recipe of "does not participate in overload resolution." And then it would behave as your comments indicate you expect it to. I would support such a proposal. But you will have to make it, don't ask me to. But I would offer you help in doing that if you would like.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With