代码之家  ›  专栏  ›  技术社区  ›  StoryTeller - Unslander Monica

有了保证的副本省略,为什么需要完全定义类?

  •  22
  • StoryTeller - Unslander Monica  · 技术社区  · 6 年前

    后续行动 this post .请考虑以下几点:

    class C;
    C foo();
    

    这是一对有效的声明。 C 仅声明函数时不需要完全定义。但如果我们要添加以下函数:

    class C;
    C foo();
    inline C bar() { return foo(); }
    

    然后突然 C类 必须是完全定义的类型。但有保证的副本删除,其成员无需。没有复制,甚至没有移动,值在其他地方初始化,并且只在调用方的上下文中销毁(到 bar )中。

    为什么?标准中有什么禁止呢?

    4 回复  |  直到 6 年前
        1
  •  6
  •   xskxzr    6 年前

    规则在于 [basic.lval]/9 以下内容:

    除非另有说明([dcl.type.simple]),否则prvalue应始终具有complete类型或void类型;…

        2
  •  14
  •   Robert Columbia yusuf dalal    6 年前

    出于兼容性和/或效率的考虑,保证的副本删除有例外。即使在可以保证复制省略的情况下,也可以复制微不足道的可复制类型。你说得对,如果这不适用,那么编译器就可以生成正确的代码,而不必知道 C ,甚至不是它的大小。但编译器确实需要知道这是否适用,为此,它仍然需要完整的类型。

    根据 https://timsong-cpp.github.io/cppwp/class.temporary 以下内容:

    15.2临时物体

    创建1个临时对象

    […]

    (1.2)--当实现需要传递或返回一个简单可复制类型的对象时(见下文),以及

    […]

    3当类类型的对象 X 如果每个复制构造函数、移动构造函数和析构函数 是琐碎的还是删除的,并且 至少有一个未删除的复制或移动构造函数,允许实现创建一个临时对象来保存函数参数或结果对象。临时对象分别由函数参数或返回值构造,函数的参数或返回对象被初始化,就像使用非删除的普通构造函数来复制临时的一样(即使该构造函数不可访问或不会被重载解析选择来执行对象的复制或移动)。[ 注: 这个纬度被授予允许类类型的对象被传递到或从寄存器中的函数返回。-- 尾注 –137;]

        3
  •  8
  •   Red.Wave P.W    6 年前

    这与拷贝省略无关。这个 foo 应该会返回一个 C 价值。只要你把一个引用或指针传递给 ,没关系。一旦你想打电话 -就像在 bar -它的参数和返回值的大小必须在手边;唯一有效的方法是表示所需类型的完整声明。 如果签名使用的是引用或指针,则所有必需的信息都存在,您可以不使用完整的类型声明。这种方法有一个名字:pimpl==指向实现的指针,它被广泛用作在封闭源代码库发行版中隐藏细节的一种方法。

        4
  •  1
  •   catnip    6 年前

    尽管答案的数量和贴在这条线上的评论的数量(它已经回答了所有 我的 个人问题),我决定张贴一个答案“为我们其他人”。我起初不明白手术的目的,但现在我明白了,所以我想我会分享。亲爱的读者,如果你知道这一切,并且感到厌烦,请继续前进。

    @xskxzr和@hvd有效地回答了这个问题,但是@hvd的帖子特别是标准的,并且假设读者知道如何按值返回(以及扩展返回 RVO )很好,我想不是每个人都这样。我以为我做到了,但我遗漏了一个重要的细节(当你仔细考虑的时候,这一点其实很明显,但我还是错过了)。

    所以这篇文章主要集中在这一点上,所以我们都能理解为什么(a)OP想知道为什么在编译时会有问题 bar() 首先,然后(b)意识到了原因。

    所以,让我们再看一遍代码。鉴于此(这是合法的,即使是不完全定义的类型):

    class C;
    C foo();
    

    为什么编译器不能编译这个(我已经删除了 inline 因为这无关紧要):

    C bar() { return foo(); }
    

    来自gcc的错误消息是:

    错误:返回类型“C类”不完整

    好吧,首先,接受的答案引用了标准中明确禁止的相关段落,所以没有什么神秘之处。但是,专栏作家(事实上也是评论沃尔特,他很快就知道了)想知道为什么。

    一开始我觉得这很明显:调用者需要为函数结果分配空间,它不知道对象有多大,所以编译器处于一个quandry中。但我错过了一个把戏 那个 在于价值回报的运作方式。

    现在对于那些不知道的人来说,按值返回类对象的工作方式是调用方为堆栈上返回的对象分配空间,并将指向它的指针作为隐藏参数传递给被调用的函数,然后由该函数构造对象,对其进行操作。

    然而 ,这个菊花链,所以如果我们有以下代码 C 打电话之前 巴() )以下内容:

    class C
    {
    public:
        int x;
    };
    
    C c = bar ();
    c.x = 4;
    

    那么空间 c 被分配 之前 巴() 电话和地址 C类 然后作为隐藏参数传递给 巴() ,然后直接传递给 foo() ,最终填充在所需位置构造对象。所以,因为 巴() 不是真的 有这个指针的任何东西(除了传递它)那么它只关心指针本身,而不是它指向什么。

    还是这样?嗯,事实上,是的。

    按值返回类对象时,小对象通常作为优化返回到寄存器(或寄存器对)中。在大多数对象足够小的情况下(稍后将详细介绍),编译器可以避免这样做。

    但现在, 巴() 需要知道这是不是什么 福() 会的 ,为此,出于各种原因,它需要查看类的完整声明。

    总之,这就是为什么编译器需要一个完全定义的类型来调用 福() ,否则它不会知道 foo ()将是预期的,因此它不知道要生成什么代码。无论如何,在大多数平台上都没有,故事结束了。

    笔记:

    1. 我查看了gcc,似乎有两个(完全逻辑的)规则用于确定类对象是在寄存器还是在寄存器对中返回:

    2. 如果读者不知道,rvo依赖于 建造 对象 在它最后的安息地 (即在呼叫方分配的位置)。这是因为有一些对象(例如 std::basic_string ,我相信)它们对在记忆中移动很敏感,所以你不能只在方便的地方构建它们,然后 memcpy 他们在别的地方。

    3. 如果在那个最终位置构造返回的对象是不可能的(因为您对返回对象的函数进行了编码),那么就不会发生rvo(怎么可能?),请参见下面的现场演示( make_no_RVO() )中。

    4. 作为点1b的一个具体例子,如果一个小对象包含的数据成员(可能)指向它自己或它的任何数据成员,那么如果不正确声明它,则按值返回它将给您带来麻烦。只需添加一个空的复制构造函数就可以了,因为从那时起它不再是简单的可复制的。但我想一般来说是这样的,不要向编译器隐藏重要信息。

    现场演示 here 是的。欢迎对本帖的所有评论,我将尽我所能予以回复。