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

1

14

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!

Praxis answered 31/12, 2015 at 7:26 Comment(2)
Having looked into this more. It seems like a std::tuple containing rvalue references should not support assignment (though the standard doesn't mention this), and std::tuple<LVALUEREF> construction from std::tuple<RVALUEREF> might be a bug in libc++. Any input would be valuable, as I'm implementing a tuple variant.Praxis
libc++ is correct and has been for some time. See Howard's answer.Praxis
A
7

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.

Abe answered 2/1, 2016 at 18:42 Comment(11)
I'm actually not sure about the first part (post-N4387, that is). The tuple(tuple<UTypes...>&& u) is removed from overload resolution, but it seems to me that tuple(const tuple<UTypes...>& u) is viable, since its SFINAE condition, is_constructible<Ti, const Ui&>, becomes is_constructible<int&, int&> with reference collapsing.Allargando
@T.C.: Answer updated to address your concern. Thanks.Abe
No, const Ui& with Ui == int&& is int&. The const applies to the reference and so is ignored, not the type referred to.Allargando
@T.C.: Ah! You may have found a defect in the working draft. The first thing to do is decide what is the intended (or desired) behavior, then the next thing to do is figure out how to say that.Abe
Fantastic answer. Also great comments, and update! Thanks for explaining the libc++ libstdc++ confusion, and... everything else. I would be happy to write a little proposal for that.Praxis
How would you feel about also requiring order of evaluation of the assignment operator matching their order of construction? The standard doesn't specifically mention an order for either, and I noticed the __swallow(tuple_leaf<Idx>::operator=...) parameter expansion in the assignment operator of libc++'s tuple. Could be done with the braced init list trick in Sean Parent's for_each_argument.Praxis
@xcvr: I would go for it if it is implementable in libc++. And I don't know if Sean's for_each_argument would do it or not. I would have to try to implement it and test it to find out (not happening for me today). If you want to work on this, I recommend first coming up with a motivating use case to show its value. I also recommend testing the waters on the discussion forum here: isocpp.org/forums. Get your motivating use cases lined up first though. You don't get a second chance to make a first impression.Abe
Fwiw I would also like to see the layout and construction order specified to be the same as the order of the template parameters. But that's not going to happen. It would be too big of an ABI break for other implementations.Abe
I will spend some time thinking about this, a great motivating use case for fixing assignment order to construction order, without the layout/construction order being defined does seem challenging.Praxis
@Allargando will you be submitting a defect report for the const Ui& issue? I would be willing to add that to my todo list.Praxis
@Praxis I think shallow constness is correct and not a defect; otherwise you can't initialize a tuple<int&> from a tuple<int&> lvalue. Personally, I think this might need a paper to properly consider all the various combinations of value/&/&& and nail down how each should behave.Allargando

© 2022 - 2024 — McMap. All rights reserved.