C++17是否禁止了C++14中允许的拷贝省略情况?

12
请看下面的内容:
struct X {
    X() {}
    X(X&&) { puts("move"); }
};
X x = X();

在C++14中,即使移动构造函数有副作用,也可以省略移动(或复制)操作,这得益于[class.copy]/31:

在以下情况下,允许省略复制/移动操作... 当一个未绑定到引用(12.2)的临时类对象将被复制/移动到具有相同cv-限定类型的类对象时

在C++17中,这个规则被删除了。相反,移动操作可以通过[dcl.init]/17.6.1保证被省略:

如果初始化表达式是prvalue,并且源类型的cv-限定版本与目标类型的类相同,则使用初始化表达式来初始化目标对象。[例子: T x = T(T(T()));调用T默认构造函数来初始化x. — 结尾 例子]

到目前为止,我所陈述的事实都是众所周知的。但现在让我们修改代码,使其读取:
X x({});

在C++14中,将执行重载决议,并使用默认构造函数将{}转换为类型为X的临时变量,然后将其移动到x中。复制省略规则允许省略此移动。
在C++17中,重载决议相同,但现在[dcl.init]/17.6.1不适用,并且来自C++14的项目已经不存在了。由于初始化程序是花括号初始化列表,因此没有初始化程序表达式。代替它的是[dcl.init]/(17.6.2):
否则,如果初始化是直接初始化,或者如果它是副本初始化,其中源类型的cv非限定版本是与目标类相同或派生类,则考虑构造函数。应枚举适构造函数用的(16.3.1.3),并通过重载决议(16.3)选择最佳构造函数。选定的构造函数被调用以初始化对象,并以其参数作为初始化程序表达式或表达式列表。如果没有构造函数适用,或者重载决议是模糊的,则初始化无效。
这似乎需要调用移动构造函数,如果标准中有其他规则可以省略它,我就不知道在哪里了。

这是Core问题2327的另一种变体。 - T.C.
@T.C. 我在公开列表中找不到那个问题。 - Brian Bi
1
我对你关于C++14的结论并不确定。{}被转换为类型为X的临时对象......然后绑定到移动构造函数的引用上。我认为你最初的引用不适用。 - Barry
@Brian 这是在假设前提条件。如果有绑定,它就不能省略,因此假设没有绑定就被省略了,这有点过了。 - Yakk - Adam Nevraumont
@Yakk 我认为唯一可能的解释是,如果源尚未绑定到引用,则可以省略复制/移动。如果仅需要将要省略的复制/移动绑定到引用,则可以省略它。根据您的解释,永远不可能省略任何内容。 - Brian Bi
显示剩余2条评论
1个回答

5

正如T.C.所指出的那样,这与CWG 2327类似:

Consider an example like:

struct Cat {};
struct Dog { operator Cat(); };

Dog d;
Cat c(d);

This goes to 11.6 [dcl.init] bullet 17.6.2:

Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (16.3.1.3 [over.match.ctor]), and the best one is chosen through overload resolution (16.3 [over.match]). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.

Overload resolution selects the move constructor of Cat. Initializing the Cat&& parameter of the constructor results in a temporary, per 11.6.3 [dcl.init.ref] bullet 5.2.1.2. This precludes the possitiblity of copy elision for this case.

This seems to be an oversight in the wording change for guaranteed copy elision. We should presumably be simultaneously considering both constructors and conversion functions in this case, as we would for copy-initialization, but we'll need to make sure that doesn't introduce any novel problems or ambiguities.

这个问题的根本原因是我们有一个错误类型的初始化器(在OP中是{},在这个例子中是d),我们需要将其转换为正确的类型(XCat),但要想弄清楚如何做到这一点,我们需要执行重载决议。这已经让我们进入移动构造函数了——在那里,我们将rvalue引用参数绑定到一个新对象上,以使这个过程发生。此时,要省略移动已经太晚了。我们已经到了那里。我们不能……回头、ctrl-z、中止中止,好的重新开始。
正如我在评论中提到的,我不确定这在C++14中是否不同。为了评估X x({}),我们必须构造一个X,将其绑定到移动构造函数的rvalue引用参数上——在那个时候我们无法省略移动,引用绑定发生在我们甚至不知道我们正在进行移动之前。

你在哪里找到那个缺陷报告的?它似乎没有出现在公开可用的问题列表中。 - Brian Bi
@Brian 目前还没有,但应该会在某个时间点上。这是基于 自然地,T.C.的报告的。 - Barry
3
我不同意这个推理,即执行过载决议需要引用绑定实际发生。我认为它只是确定应调用移动构造函数,因为移动构造函数是最佳可行函数,但编译器随后允许省略它,因为引用绑定尚未实际发生。 - Brian Bi
@Brian 现在公开。 - Barry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接