19

考虑以下代码:

#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]

如果 T1 或 T2 是类类型,并且 T1 与 T2 没有引用相关,则使用通过用户定义转换([ dcl.init ], [over.match.copy], [over.match.conv]); 如果相应的非参考复制初始化格式错误,则程序格式错误。调用转换函数的结果,如针对非引用复制初始化所描述的,然后用于直接初始化引用。对于这种直接初始化,不考虑用户定义的转换

复制初始化

否则(即,对于剩余的复制初始化情况),可以从源类型转换到目标类型或(当使用转换函数时)到其派生类的用户定义转换被枚举,如 [over. match.copy],并通过重载决议([over.match])选择最佳的。如果转换无法完成或不明确,则初始化格式错误。以初始化表达式作为参数调用所选函数;如果函数是构造函数,则调用是目标类型的 cv 非限定版本的纯右值,其结果对象由构造函数初始化。该调用用于根据上述规则直接初始化作为复制初始化目标的对象。

按照标准, 的类型aint,初始化引用的类型是Data,所以 fromintData用户定义的转换被认为是使用通过用户定义转换复制初始化类型为“cv1 T1”的对象的规则. 意思是Data const& d_rf = a;可以翻译成Data temporary = a; Data const& d_rf = temporary;。因为Data temporary = a;,即使存在复制省略,也必须检查复制/移动构造函数是否可用,但复制构造函数已经被删除,class Data为什么还能编译呢?

以下是 来自 enseignement 的引用的标准
复制初始化的一些引用

从 cppreference 复制引用的初始化

如果引用是左值引用:

如果 object 是左值表达式,并且它的类型是 T 或从 T 派生,并且具有相同或更少的 cv 限定,则引用绑定到由左值标识的对象或其基类子对象。
如果 object 是左值表达式,并且其类型可隐式转换为 T 或派生自 T 的类型,同等或更少 cv 限定,则源类型及其返回左值的基类的非显式转换函数考虑参考,并通过重载决议选择最好的参考。然后将引用绑定到由转换函数返回的左值标识的对象(或其基类子对象)

否则,如果引用是对 const 的右值引用或左值引用:

如果 object 是 xvalue、类纯右值、数组纯右值或 T 或从 T 派生的函数左值类型,同样或更少 cv 限定,则引用绑定到初始化表达式的值或其基础子对象。
如果 object 是一个类类型表达式,可以隐式转换为 xvalue、类纯右值或类型为 T 或派生自 T 的函数值,同样或更少 cv 限定,则引用绑定到结果转换或其基础子对象。
否则,将构造一个 T 类型的临时对象并从对象复制初始化。然后引用绑定到这个临时。应用复制初始化规则(不考虑显式构造函数)。
[例子:
常量 std::string& rs = "abc"; // rs 指的是从 char 数组 中初始化的临时复制]

更新:

我们考虑N337下的代码

根据标准,值a的类型是int,引用所指的目的类型是Data,所以编译器需要Data通过拷贝初始化生成一个临时的类型。这里毫无疑问,所以我们专注于拷贝初始化。源类型是int目标类型是Data,这种情况符合:

否则(即,对于剩余的复制初始化情况),可以从源类型转换到目标类型或(当使用转换函数时)到其派生类的用户定义转换序列被枚举,如 13.3 中所述。 1.4,最好的一个是通过重载决议(13.3)选择的。如果转换无法完成或不明确,则初始化格式错误。以初始化表达式作为参数调用所选函数;如果函数是构造函数,则调用初始化目标类型的 cv 非限定版本的临时版本。临时是prvalue。然后根据上述规则,调用的结果(对于构造函数的情况是临时的)用于直接初始化作为复制初始化目标的对象。在某些情况下,允许实现通过将中间结果直接构造到正在初始化的对象中来消除这种直接初始化中固有的复制;

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

上面的规则在这里:

— 如果初始化是直接初始化,或者如果是复制初始化,其中源类型的 cv 非限定版本与目标类相同或派生类,则考虑构造函数。枚举了适用的构造函数(13.3.1.3),并通过重载决议(13.3)选择最佳构造函数。调用如此选择的构造函数来初始化对象,使用初始化表达式或表达式列表作为其参数。如果没有构造函数适用,或者重载决议不明确,则初始化格式错误。

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

4

6 回答 6

5

这个问题已由Issue 1604解决,并且提议的解决方案似乎确认此类代码应该是格式错误的,因此我将其视为编译器错误。

幸运的是,从 C++17 开始,由于保证复制省略,这段代码变得格式良好,这与编译器一致。

于 2020-02-02T09:00:21.800 回答
4

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

否则,将构造一个 T 类型的临时对象并从对象复制初始化。然后引用绑定到这个临时。应用复制初始化规则(不考虑显式构造函数)。

因此,构造了一个临时类型T。这个临时是从给定对象复制初始化的。好的...这是怎么工作的?

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

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

任何时候都不会尝试调用任何已删除的函数。仅仅因为它被称为“复制初始化”并不意味着将调用复制构造函数。它被称为“复制初始化”,因为它(通常)是使用=符号引发的,因此它看起来像“复制”。

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

于 2020-01-23T04:25:01.087 回答
2

接受的答案看起来无关紧要;序列就像看起来一样简单。不涉及复制/移动构造函数或优化;所有主题都严格无关。使用转换 ctor 从“int”构造临时“数据”。prvalue 然后绑定到“const”左值引用。就这些。如果这看起来不对,那么我们正在讨论不同的编程语言;我当然是在谈论 C++。

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

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

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

最好的,调频。

于 2020-02-05T12:12:43.160 回答
0

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

于 2020-02-07T06:55:36.960 回答
0

考虑这段代码:

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 以大括号或等于初始化器或条件 (6.4) 的 = 形式发生的初始化,以及在参数传递、函数返回、抛出异常 (15.1)、处理异常 (15.3) 和聚合中发生的初始化成员初始化(8.5.1),称为复制初始化。[注意:复制初始化可能会调用移动(12.8)。——尾注]

16 表单中发生的初始化

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

以及 new 表达式 (5.3.4)、static_cast 表达式 (5.2.9)、功能符号类型转换 (5.2.3)、mem-initializers (12.6.2) 和条件的括号初始化列表形式称为直接初始化。

然后

如果初始化是直接初始化,或者如果是复制初始化,其中源类型的 cv 非限定版本与目标类相同或派生类,则考虑构造函数。

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

否则(即,对于剩余的复制初始化情况),可以从源类型转换到目标类型或(当使用转换函数时)到其派生类的用户定义转换序列被枚举,如 13.3 中所述。 1.4,最好的一个是通过重载决议(13.3)选择的。如果转换无法完成或不明确,则初始化格式错误。以初始化表达式作为参数调用所选函数;如果函数是构造函数,则调用初始化目标类型的 cv 非限定版本的临时版本。临时是prvalue。然后根据上面的规则,调用的结果(这是构造函数案例的临时结果)用于直接初始化,作为复制初始化目标的对象。在某些情况下,允许实现通过将中间结果直接构造到正在初始化的对象中来消除这种直接初始化中固有的复制

因此,如果常量5不是Dataor类型Data2,那么 forbb2复制构造函数在复制初始化期间通过直接初始化临时对象转换为适当类型后被调用5,该临时对象可以绑定到const Data&构造函数的参数,但不能绑定到构造函数Data&时候选人被考虑。

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

于 2020-02-07T07:29:34.897 回答
-1

在 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复制/移动构造函数

  1. [注意:当移动构造函数没有被隐式声明或显式提供时,否则会调用移动构造函数的表达式可能会调用复制构造函数。——尾注]

唉,copy-ctor 已被删除。

于 2020-01-20T09:59:59.463 回答