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

xcv*_*cvr 12 c++ assignment-operator c++11 stdtuple

看起来std::tuple包含一个或多个引用在构造和赋值方面具有意外行为(尤其是复制/移动构造和复制/移动分配).它与两者的行为std::reference_wrapper(更改引用的对象)和具有成员引用变量的结构(赋值运算符已删除)不同.它允许方便的std::tiepython像多个返回值,但它也允许明显不正确的代码,如下所示(链接在这里):

#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;
}
Run Code Online (Sandbox Code Playgroud)

该标准似乎要求或强烈暗示20.4.2.2.9最新工作草案的副本(ish)分配部分中的此行为(Ti&将折叠为左值ref):

template <class... UTypes> tuple& operator=(const tuple<UTypes...>& u);

9 要求: sizeof...(Types) == sizeof...(UTypes)并且is_assignable<Ti&, const Ui&>::value对所有人都是如此i.

10 效果:将u的每个元素分配给*this的相应元素.

11 返回:*this

虽然移动(ish)构造部分20.4.2.1.20不太清楚(is_constructible<int&, int&&>返回false):

template <class... UTypes> EXPLICIT constexpr tuple(tuple<UTypes...>&& u);

18 要求: sizeof...(Types) == sizeof...(UTypes).

19 效果:对于所有i,构造函数初始化with 的i第th个元素.*thisstd::forward<Ui>(get<i>(u))

20 备注:除非is_constructible<Ti, Ui&&>::value对所有人都适用,否则此构造函数不应参与重载决策i.构造函数是explicitif和only if is_convertible<Ui&&, Ti>::value至少为false i.

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

问题是,为什么这种行为需要?此外,如果标准的其他部分正在发挥作用,或者我误解了它,请解释我出错的地方.

谢谢!

How*_*ant 7

构造函数

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

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

另请注意:

std::is_constructible<int&, int&&>::value == false
Run Code Online (Sandbox Code Playgroud)

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

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

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

使用最新的gcc,根据此规范错误地编译:

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

最新的VS-2015根据此规范错误编译:

http://webcompiler.cloudapp.net

(这最后一个链接不包含正确的代码,您必须将其粘贴).

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

从这次调查看来,只有 libc ++与规范和期望一致.但这不是故事的结局.

您正确引用的最新工作草案规范与C++ 14规范不同,它看起来像这样:

template <class... UTypes> constexpr tuple(tuple<UTypes...>&& u);

要求:sizeof...(Types) == sizeof...(Types).is_constructible<Ti, Ui&&>::valuetrue所有.

效果:对于所有i,初始化with 的第i个元素.*thisstd::forward<Ui>(get<i>(u))

备注:此构造函数不应参与重载决策,除非每个类型都UTypes可以隐式转换为其对应的类型Types.

这是N4387,改变本说明书 C++ 14.

在C++ 14(以及在C++ 11为好),有责任的上客户端,以确保is_constructible<Ti, Ui&&>::valuetrue所有.如果不是,那么你有不确定的行为.

因此,您的第一行代码是C++ 11和C++ 14中的未定义行为,而C++ 1z工作草案中则不正确.对于未定义的行为,任何事情都可能发生 因此,所有libc ++,libstdc ++和VS都符合C++ 11和C++ 14规范.


以下TC的评论启发了更新:

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

std::is_constructible<int&, const int&>::value == false
Run Code Online (Sandbox Code Playgroud)

唉,TC可能在工作草案中发现了一个缺陷.实际规格说:

std::is_constructible<int&, int&>::value == true
Run Code Online (Sandbox Code Playgroud)

因为const它适用于参考,而不是int.


似乎libstdc ++和VS尚未实现由N4387指定的后C++ 14规范,并且它几乎指定了您的注释应该发生的内容.libc ++多年来一直实施N4387,并作为该提案的实施证明.

分配

这需要:

std::is_assignable<int&, const int&>::value == true
Run Code Online (Sandbox Code Playgroud)

事实上,这在您的示例分配中是正确的.在这行代码中.但是我认为可以提出一个论点,它应该要求:

std::is_assignable<int&&, const int&>::value == true
Run Code Online (Sandbox Code Playgroud)

这是假的.现在,如果我们只进行了此更改,则您的赋值将是未定义的行为,因为规范的这一部分位于Requires子句中.因此,如果您确实想要这样做,则需要将规范移动到备注子句中,其子句为"不参与重载决策".然后它会表现为您的评论表明您期望它.我会支持这样的提议.但你必须做到,不要问我.但如果您愿意,我会帮您做到这一点.

  • 我实际上不确定第一部分(后N4387,即).`tuple(元组<UTypes ...> && u)`从重载解析中删除,但在我看来`tuple(const tuple <UTypes ...>&u)`是可行的,因为它的SFINAE条件, `is_constructible <Ti,const Ui&>`,变为`is_constructible <int&,int&>`并引用折叠. (2认同)
  • 不,`const Ui&`与`Ui == int &&`是`int&`.`const`适用于引用,因此被忽略,而不是引用的类型. (2认同)