为什么标准允许左值引用元组分配给右值引用元组?
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 i
th 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&&>::value
是 true
所有 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." 的配方,然后它会按照您的评论所期望的那样运行它到。我会支持这样的提议。但你必须成功,不要要求我这样做。但如果你愿意,我会为你提供帮助。
似乎包含一个或多个引用的 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)
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
虽然 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 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
.
这些不是唯一受影响的小节。
问题是,为什么需要这种行为?另外,如果标准的其他部分在起作用,或者我误解了它,请解释我哪里出错了。
谢谢!
构造函数
让我们从构造函数(您的第一行)开始:
我们都知道(只是澄清一下),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
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
.
N4387在 C++14 之后更改了规范。
在 C++14(以及 C++11 中)中,客户端 有责任确保 is_constructible<Ti, Ui&&>::value
是 true
所有 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." 的配方,然后它会按照您的评论所期望的那样运行它到。我会支持这样的提议。但你必须成功,不要要求我这样做。但如果你愿意,我会为你提供帮助。