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

时间:2015-12-31 07:26:18

标签: c++ c++11 assignment-operator stdtuple

似乎包含一个或多个引用的std::tuple在构造和赋值方面具有意外行为(尤其是复制/移动构造和复制/移动分配)。它与std::reference_wrapper(更改引用的对象)和带有成员引用变量的结构(删除赋值运算符)的行为不同。它允许方便的std::tie python像多个返回值,但它也允许明显不正确的代码,如下面的(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&将折叠为左值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,构造函数使用i初始化*this的{​​{1}}元素。

     

20 备注:除非 std::forward<Ui>(get<i>(u)) 对所有is_constructible<Ti, Ui&&>::value都为真,否则此构造函数不应参与重载决策。当且仅当i对于至少一个explicit为假时,构造函数为is_convertible<Ui&&, Ti>::value

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

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

谢谢!

1 个答案:

答案 0 :(得分:7)

构造函数

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

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

另请注意:

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

在所有编译器平台上。因此,根据您引用的规范,这与最新的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)。对于所有 i is_constructible<Ti, Ui&&>::valuetrue

     

效果:对于所有 i ,初始化*this ith 元素   std::forward<Ui>(get<i>(u))

     

备注:此构造函数不应参与重载解析   除非UTypes中的每个类型都可以隐式转换为它   Types中的相应类型。

在 C ++ 14之后,N4387改变了规范

在C ++ 14中(以及在C ++ 11中),onus位于客户端上,以确保所有is_constructible<Ti, Ui&&>::value为true > I 的。如果不是,那么你有不明确的行为。

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

更新受以下T.C.评论的启发:

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

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

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

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

因为const已应用于参考,而不是int

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

作业

这需要:

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

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

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

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