在引用初始化中使用删除的复制构造函数复制初始化

Copy initialization with deleted copy constructor in reference initialization

考虑以下代码:

#include <iostream>
class Data{
public:
    Data() = default;
    Data(Data const&) = delete;
    Data(int) {

    }
};
int main(){
  int a = 0;
  const std::string& rs = "abc"; // rs refers to temporary copy-initialized from char array
  Data const& d_rf = a;          // #2 but here can be complied
  // accroding to the standard, the reference in #2 is bound to a temporary object, the temporary is copy-initialized from the expression
}

[dcl.init.ref]

If T1 or T2 is a class type and T1 is not reference-related to T2, user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion ([dcl.init], [over.match.copy], [over.match.conv]); the program is ill-formed if the corresponding non-reference copy-initialization would be ill-formed. The result of the call to the conversion function, as described for the non-reference copy-initialization, is then used to direct-initialize the reference. For this direct-initialization, user-defined conversions are not considered

Copy initialization

Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversions that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in [over.match.copy], and the best one is chosen through overload resolution ([over.match]). If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call is a prvalue of the cv-unqualified version of the destination type whose result object is initialized by the constructor. The call is used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization.

按照标准,a的类型是int,初始化引用的类型是Data,所以从intData, 用户定义的转换被认为是使用“cv1 T1”类型对象的复制初始化规则 由用户定义的转换 .这意味着 Data const& d_rf = a; 可以翻译成 Data temporary = a; Data const& d_rf = temporary;。对于Data temporary = a;,即使存在copy elision,也必须检查copy/move构造函数是否可用,但是class Data的copy constructor被删除了,为什么还能编译?

这里有一些标准的引用
Copy initialization of reference 来自 enseignement

Copy initialization of reference 来自 cppreference

If the reference is an lvalue reference:

If object is an lvalue expression, and its type is T or derived from T, and is equally or less cv-qualified, then the reference is bound to the object identified by the lvalue or to its base class subobject.
If object is an lvalue expression, and its type is implicitly convertible to a type that is either T or derived from T, equally or less cv-qualified, then the non-explicit conversion functions of the source type and its base classes that return lvalue references are considered and the best one is selected by overload resolution. The reference is then bound to the object identified by the lvalue returned by the conversion function (or to its base class subobject)

Otherwise, if the reference is either rvalue reference or lvalue reference to const:

If object is an xvalue, a class prvalue, an array prvalue, or a function lvalue type that is either T or derived from T, equally or less cv-qualified, then the reference is bound to the value of the initializer expression or to its base subobject.
If object is a class type expression that can be implicitly converted to an xvalue, a class prvalue, or a function value of type that is either T or derived from T, equally or less cv-qualified, then the reference is bound to the result of the conversion or to its base subobject.
Otherwise, a temporary of type T is constructed and copy-initialized from object. The reference is then bound to this temporary. Copy-initialization rules apply (explicit constructors are not considered).
[example:
const std::string& rs = "abc"; // rs refers to temporary copy-initialized from char array ]

更新:

我们考虑N337下的代码

根据标准,值a的类型是int,引用指向的目标类型是Data,所以编译器需要生成一个通过复制初始化临时类型Data。这里没有疑问,所以我们重点关注复制初始化。源类型为int,目的类型为Data,这种情况符合:

Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call initializes a temporary of the cv-unqualified version of the destination type. The temporary is a prvalue. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization. In certain cases, an implementation is permitted to eliminate the copying inherent in this direct-initialization by constructing the intermediate result directly into the object being initialized;

注意粗体部分,它并不意味着值 int 直接由 Data::Data(int) 初始化临时值。这意味着,int首先被Data::Data(int)转换为Data,然后这个结果直接初始化临时对象,即作为复制初始化目标的对象 这里。如果我们用代码来表示粗体部分,就像Data temporary(Data(a)).

以上规则在这里:

— 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 (13.3.1.3), and the best one is chosen through overload resolution (13.3). 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.

请返回 Data temporary(Data(a))。显然,copy/move 构造函数是参数 Data(a) 的最佳匹配。但是,Data(Data const&) = delete;,所以copy/move构造函数不可用。为什么编译器不报错?

在 C++11 中编译代码时(您使用了 =default=delete 因此它至少是 C++11)错误在第 1 行,另一个(# 2)没有问题:

$ g++ -Wall --std=c++11 -o toto toto.cpp
toto.cpp:14:8: error: copying variable of type 'Data' invokes deleted constructor
  Data d = a;  //#1
       ^   ~
toto.cpp:5:5: note: 'Data' has been explicitly marked deleted here
    Data(Data const&) = delete;
    ^
1 error generated.

对于#1,首先在 [class.conv.ctor] 的帮助下制作 [over.match.copy]。因此它被转换为Data d = Data(a)。其次,由于您处于移动语义编译器的范围内,因此无法找到正确的 ctor,因为:

11.4.4.2 Copy/Move constructors

  1. [ Note: When the move constructor is not implicitly declared or explicitly supplied, expressions that otherwise would have invoked the move constructor may instead invoke a copy constructor. — end note ]

唉,抄袭者被删了

让我们来看看标准是怎么说的:

Otherwise, a temporary of type T is constructed and copy-initialized from object. The reference is then bound to this temporary. Copy-initialization rules apply (explicit constructors are not considered).

所以,构造了一个T类型的临时对象。该临时对象是从给定对象复制初始化的。好的...那是如何工作的?

好吧,您引用了解释给定值的复制初始化如何工作的规则。它将尝试调用用户定义的转换,通过筛选 T 的适用构造函数和值的转换运算符(没有任何,因为它是 int 类型)。 T 上有一个隐式转换构造函数,它采用类型 int 的对象。因此调用构造函数来初始化对象。

然后根据您引用的规则将引用绑定到该临时文件。

任何时候 都不会尝试调用任何已删除的函数。仅仅因为它被称为 "copy-initialization" 并不意味着副本 constructor 将被调用。它被称为 "copy-initialization" 因为它(通常)是使用 = 符号激发的,因此它看起来像 "copying".

Data d = a; 不起作用的原因是因为 C++11 定义此操作首先将 a 转换为 Data 临时值,然后初始化 d与那个临时的。也就是说,它本质上等同于 Data d = Data(a);。后面的初始化将(假设地)调用复制构造函数,从而导致错误。

此问题已由 Issue 1604 解决,建议的解决方案似乎确认此类代码格式不正确,因此我将其视为编译器错误。

幸运的是,从 C++17 开始,此代码变得格式正确 ,这与编译器一致。

接受的答案看起来无关紧要;这个序列就像它看起来的一样简单。不涉及 copy/move 构造函数或优化;所有的主题都是无关紧要的。临时 'Data' 是从 'int' 使用转换构造器构建的。然后纯右值绑定到 'const' 左值引用。就这些。如果这看起来不对,那么我们正在讨论不同的编程语言;我当然是在谈论 C++。

PS:我无法引用标准,因为我买不起。

编辑================================

'=' 只是调用未标记为 'explicit' 的单个参数 ctor 的另一种方式。它与花括号或圆括号相同 - 只要 ctor 接受单个参数,除非 ctor 是 'explicit'。 没有人通过阅读标准来学习编程;它适用于编译器设计人员。

最好的, 调频

我倾向于同意@Red.Wave——临时对象是使用Data::Data(int)构造的,然后引用"d_rf"用它的地址初始化。这里根本不涉及复制构造函数。

考虑这段代码:

class Data{
public:
    Data() = default;
    Data(Data const&) = delete;
    Data(int) {
    }
};

class Data2{
public:
    Data2() = default;
    Data2(Data &) = delete;
    Data2(int) {
    }
};

int main()
{
    Data a {5};
    Data b  = 5;

    Data2 a2{5};
    Data2 b2  = 5;
}

直到 C++17 标准只有 b 的初始化是错误格式的。这里使用的两种初始化形式说明如下(抄自N4296):

15 The initialization that occurs in the = form of a brace-or-equal-initializer or condition (6.4), as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1), is called copy-initialization. [ Note: Copy-initialization may invoke a move (12.8). —end note ]

16 The initialization that occurs in the forms

T x(a); 
T x{a}; 

as well as in new expressions (5.3.4), static_cast expressions (5.2.9), functional notation type conversions (5.2.3), mem-initializers (12.6.2), and the braced-init-list form of a condition is called direct-initialization.

然后

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.

不是我们的情况,继续下一段

Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call initializes a temporary of the cv-unqualified version of the destination type. The temporary is a prvalue. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization. In certain cases, an implementation is permitted to eliminate the copying inherent in this direct-initialization by constructing the intermediate result directly into the object being initialized

因此,假设常量 5 不是 DataData2 类型,那么对于 bb2 复制构造函数被调用在通过直接初始化临时对象将 5 转换为正确类型后的复制初始化期间,可以绑定到构造函数的 const Data& 参数,但在考虑候选构造函数时不能绑定到 Data&

b 已删除其复制构造函数,因此初始化格式错误。 b2 已被禁止仅从非 const 对象初始化,调用不能绑定到这种情况。根据 C++11/14 规则,没有发生复制省略。