【问题标题】:Copy initialization with deleted copy constructor in reference initialization在引用初始化中使用已删除的复制构造函数进行复制初始化
【发布时间】:2020-05-06 06:36:50
【问题描述】:

考虑以下代码:

#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 没有引用相关,则使用“cv1 T1”类型对象的复制初始化规则考虑用户定义的转换通过用户定义的转换([dcl.init]、[over.match.copy]、[over.match.conv]);如果相应的非参考复制初始化格式错误,则程序格式错误。调用转换函数的结果,如针对非引用复制初始化所描述的,然后用于直接初始化引用。对于这种直接初始化,不考虑用户定义的转换

Copy initialization

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

按照标准,a的类型是int,初始化引用的类型是Data,所以从intData考虑自定义转换通过用户定义的转换使用复制初始化类型“cv1 T1”对象的规则。这意味着Data const&amp; d_rf = a; 可以翻译成Data temporary = a; Data const&amp; d_rf = temporary;。对于Data temporary = a;,即使复制省略存在,也必须检查复制/移动构造函数是否可用,但复制构造函数class Data已被删除,为什么还能遵守?

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

Copy initialization of reference 来自 cppreference

如果引用是左值引用:

如果对象是左值表达式,并且它的类型是 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 数组中的临时 copy-initialized ]

更新:

我们考虑N337下的代码

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

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

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

上面的规则在这里:

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

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

【问题讨论】:

  • 为什么你认为复制初始化是为第 2 行完成的。我认为充其量 operator& 被调用,但在你的情况下没有定义。
  • 请注意,#1 自 C++17 起有效
  • @Klaus,源类型与目标类型不兼容
  • @LukaRahne 从 int 到 Data ,需要自定义转换
  • @Mannoj 注意,源类型与目标类型不兼容

标签: c++ c++11 language-lawyer implicit-conversion reference-binding


【解决方案1】:

Issue 1604 解决了这个问题,并且提议的解决方案似乎证实了这样的代码应该是格式错误的,所以我认为它是一个编译器错误。

幸运的是,从 C++17 开始,这段代码变成了格式良好的because of guaranteed copy elision,这与编译器一致。

【讨论】:

  • 是的,c++17 保证复制省略,但在同一个编译器中,Data d = 0 仍然是格式错误的,为什么这种情况与初始化引用的复制初始化行为不同
  • @jackX 不知道,也许我们需要检查编译器的逻辑才能看到原因。
【解决方案2】:

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

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

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

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

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

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

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

【讨论】:

  • @NicoBolas 关键点是从intData,隐式转换是Data::Data(int) 或其他任何东西,如果转换是Data::Data(int),即使没有@987654337 @,没关系,但是如上面我引用的标准,T const&amp; rf = object,如果对象不是T或者是从T派生的,转换表单对象到T需要使用copy-initialization生成临时值T类型的,然后引用绑定到临时值,如果我看错了标准,请用相关标准纠正我,谢谢
  • 我对 T 类型的临时对象的理解是构造和复制初始化的对象是 T temporary = object,正如你所说的“复制初始化”意味着“使用 = 符号”,你同意我的观点吗?现在,我们替换参数,结果是Datatemporary = a,那么引用绑定到temporary,所以对于Data temporary = a,即使存在复制省略,也必须检查复制/移动构造函数
  • @jackX: "将表单对象转换为T需要使用复制初始化来生成T类型的临时值" 尝试构造时调用复制构造函数T 类型的对象,具有T 类型的一些左值。我们还没有Data;我们只有一个int。所以我们根据我概述的规则从int 复制初始化Data 临时。绝不会尝试调用复制构造函数。
  • 涉及两个临时对象。构造了一个T 类型的临时t1,并且引用绑定到t1。要从a复制初始化t1,需要按照规则构造另一个临时的t2,然后从t2直接初始化t1。问题出在从t2直接初始化t1的过程。
【解决方案3】:

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

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

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

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

最好, 调频。

【讨论】:

  • 关键点在于复制初始化(即一个临时的“数据”是从一个“int”构造的)
  • 我只是看不到问题所在。是由于错误地引用了对象还是什么?即使使用 c99 和私有副本 ctor(=delete 语法不存在),代码也必须编译。如果它没有编译,我会询问有关破坏代码的新规则的信息。
  • 复制初始化需要检查复制/移动构造函数在C++11中是否可用
  • 大号。显然误将对象作为参考。删除该行中的&符号,我们可以讨论这种情况。
  • 我从标准中找到了参考引用,尽管缺乏正确的编译原因(它们分布在大量文档中),因为在 C++14 的情况下不会发生省略,在 C+ +11 它不会发生。
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    考虑这段代码:

    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}; 
    

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

    然后

    如果初始化是直接初始化,或者如果是 复制初始化,其中源的 cv 不合格版本 type 与该类的类相同或派生类 目的地,构造函数被考虑。

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

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

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

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

    【讨论】:

    • 但是你的案例都不适用,因为 OP 正在初始化一个引用。
    【解决方案6】:

    在 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已被删除。

    【讨论】:

    • here 是关于引用复制初始化的标准。否则,将构造一个 T 类型的临时对象并从对象复制初始化。然后引用绑定到这个临时。复制初始化规则适用(不考虑显式构造函数)。这意味着被引用绑定的临时对象被初始化为与#1相同的形式@
    • 被引文档不标准(似乎是 cppreference 的本地副本)。 anacongua 的回答告诉你同样的情况。
    • 如果cppreference不是标准的,this一定是,我这里复制部分
    • 如果 T1 或 T2 是类类型,并且 T1 与 T2 没有引用相关,则使用对象的 copy-initialization 规则考虑用户定义的转换通过用户定义的转换键入“cv1 T1”([dcl.init],[over.match.copy],[over.match.conv]);如果相应的非参考复制初始化格式错误,则程序格式错误。调用转换函数的结果,如针对非引用复制初始化所描述的,然后用于直接初始化引用。对于这种直接初始化,不考虑用户定义的转换
    • 注意粗体部分,表示引用绑定的结果是使用copy-initialization构造的
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-28
    • 1970-01-01
    • 2018-03-02
    • 2012-08-14
    相关资源
    最近更新 更多