Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the standard allow a tuple of rvalue references to be assigned to by a tuple of lvalue references?

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) and is_assignable<Ti&, const Ui&>::value is true for all i.

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 the ith element of *this with std::forward<Ui>(get<i>(u)).

20 Remarks: This constructor shall not participate in overload resolution unless is_constructible<Ti, Ui&&>::value is true for all i. The constructor is explicit if and only if is_convertible<Ui&&, Ti>::value is false for at least one i.

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!

like image 708
xcvr Avatar asked Dec 31 '15 07:12

xcvr


1 Answers

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 is true for all i.

Effects: For all i, initializes the ith element of *this with std::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 in Types.

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.

like image 60
Howard Hinnant Avatar answered Oct 23 '22 08:10

Howard Hinnant