为什么标准允许左值引用元组分配给右值引用元组?

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

似乎包含一个或多个引用的 std::tuple 在构造和赋值方面有意外行为(尤其是 copy/move 构造和 copy/move 赋值)。它不同于 std::reference_wrapper(更改引用的对象)和具有成员引用变量的结构(删除赋值运算符)的行为。它允许方便的 std::tie python 像多个 return 值,但它也允许明显不正确的代码,如以下 (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;
}

该标准似乎要求或强烈暗示最新工作草案的复制(ish)分配部分20.4.2.2.9中的这种行为(Ti& 将折叠为左值引用):

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

虽然 move(ish) 构造部分 20.4.2.1.20 不太清楚 (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.

这些不是唯一受影响的小节。

问题是,为什么需要这种行为?另外,如果标准的其他部分在起作用,或者我误解了它,请解释我哪里出错了。

谢谢!

构造函数

让我们从构造函数(您的第一行)开始:

我们都知道(只是澄清一下),decltype(std::forward_as_tuple(9))std::tuple<int&&>

另请注意:

std::is_constructible<int&, int&&>::value == false

在所有编译器平台上。因此,根据您引用的与最新的C++1z工作草案一致的规范,您第一行代码中的构造不应参与重载决议。

在使用 libc++ 的 clang 中,它无法根据此规范正确编译:

http://melpon.org/wandbox/permlink/7cTyS3luVn1XRXGv

使用最新的 gcc,它根据这个规范编译不正确:

http://melpon.org/wandbox/permlink/CSoB1BTNF3emIuvm

并且在最新的 VS-2015 中,它根据这个规范编译不正确:

http://webcompiler.cloudapp.net

(最后的link不包含正确的代码,您必须将其粘贴进去)。

在你的 coliru link 中,虽然你使用的是 clang,但 clang 隐含地使用 gcc 的 libstdc++ 作为 std::lib,所以你的结果与我的一致。

根据这项调查,只有 libc++ 符合规范和您的期望。但这并不是故事的结局。

您正确引用的最新工作草案规范与 C++14 规范不同,后者如下所示:

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.

N4387在 C++14 之后更改了规范。

在 C++14(以及 C++11 中)中,客户端 有责任确保 is_constructible<Ti, Ui&&>::valuetrue 所有 i。如果不是,则说明您有未定义的行为。

所以你的第一行代码在 C++11 和 C++14 中是未定义的行为,在 C++1z 工作草案中是 ill-formed。对于未定义的行为,任何事情都可能发生。所以所有的 libc++、libstdc++ 和 VS 都符合 C++11 和 C++14 规范。


更新 灵感来自 T.C. 下面的评论:

请注意,从重载决策中删除这个构造函数可能允许表达式使用另一个构造函数,例如 tuple(const tuple<UTypes...>& u)。但是,此构造函数也通过类似的 Remarks 子句从重载决策中删除。

std::is_constructible<int&, const int&>::value == false

唉,T.C。可能在工作草案中发现了缺陷。实际规格说:

std::is_constructible<int&, int&>::value == true

因为 const 应用于引用,而不是 int


看来 libstdc++ 和 VS 尚未实现 post-C++14 规范,由 N4387 指定,它几乎指定了您的评论表明应该发生的事情。 libc++ 已实施 N4387 多年,并作为该提案的 proof-of-implementation。

赋值

这需要:

std::is_assignable<int&, const int&>::value == true

在您的示例作业中,这实际上是正确的。在这一行代码中。但是我认为可以提出一个论点,它应该改为要求:

std::is_assignable<int&&, const int&>::value == true

这是错误的。现在,如果我们只进行此更改,您的分配将是未定义的行为,因为规范的这一部分位于 Requires 子句中。因此,如果您真的想正确执行此操作,则需要将规范移动到 Remarks 子句中,其中包含 "does not participate in overload resolution." 的配方,然后它会按照您的评论所期望的那样运行它到。我会支持这样的提议。但你必须成功,不要要求我这样做。但如果你愿意,我会为你提供帮助。