【问题标题】:With guaranteed copy elision, why does the class need to be fully defined?在保证复制省略的情况下,为什么需要完全定义类?
【发布时间】:2018-06-27 08:31:54
【问题描述】:

this post 的后续行动。考虑以下几点:

class C;
C foo();

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

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

然后突然C 需要是一个完全定义的类型。但是通过保证复制省略,不需要它的任何成员。没有复制甚至移动,该值在其他地方初始化,并且仅在调用者的上下文中销毁(到bar)。

那为什么?标准中有什么禁止的?

【问题讨论】:

  • 你不是把编译器放在需要知道 sizeof (C) 才能调用foo() 的位置吗?即使省略了复制/移动,调用foo() 的结果也必须到达某处
  • @PaulSanders 这不是答案吗?
  • 问题现在是opinion-based。但是您可以按照this answer 中的建议轻松对其进行编辑以使其更清晰。
  • 对。我觉得我应该向任何相关方道歉。我确实想要书面的法律条文。向任何拒绝回答的人道歉,因为它似乎是基于意见的。谢谢大家对理由的看法!这也是无价之宝。
  • @xskxzr 不是。我认为沃尔特和我都只是想在我们的措辞上保持礼貌。尽管如此,您已经导航到标准中的相关段落(时髦的绿色突出显示和所有:)

标签: c++ language-lawyer c++17


【解决方案1】:

出于兼容性和/或效率的原因,保证复制省略有例外。即使在其他情况下可以保证复制省略的情况下,也可以复制普通可复制类型。你是对的,如果这不适用,那么编译器将能够在不知道C 的任何细节的情况下生成正确的代码,甚至不知道它的大小。但是编译器确实需要知道这是否适用,为此,它仍然需要类型是完整的。

根据https://timsong-cpp.github.io/cppwp/class.temporary

15.2 临时对象 [class.temporary]

1个临时对象被创建

[...]

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

[...]

3 当类类型X的对象被传递给函数或从函数返回时,如果X的每个复制构造函数、移动构造函数和析构函数要么是平凡的要么被删除,并且X至少有一个未删除的复制或移动构造函数,允许实现创建一个临时对象来保存函数参数或结果对象。临时对象分别由函数参数或返回值构造,并且函数的参数或返回对象被初始化,就好像使用未删除的普通构造函数复制临时对象(即使该构造函数不可访问或不会被选中)通过重载决议来执行对象的复制或移动)。 [ 注意: 授予此权限以允许将类类型的对象传递给寄存器中的函数或从函数返回。 -- 尾注 ]

【讨论】:

  • 一些带有时髦绿色突出显示的友好链接:1.23。 (我喜欢这个人的网站)。
  • 每次我回过头来,我意识到你设法用尽可能少的字数抓住了这个问题的核心。很好,请忽略我之前的一些比较幼稚的 cmets。
  • "你说得对,如果这不适用,那么编译器将能够在不知道 C 的任何细节的情况下生成正确的代码。"这显然是错误的,对我来说没有任何意义。 @xskxzr 接受的答案对此非常清楚(-1)。
【解决方案2】:

这与复制省略无关。 foo 应该返回一个 C 值。只要您只是传递一个引用或指向foo 的指针,就可以了。一旦你尝试调用foo——就像bar中的情况一样——它的参数和返回值的大小必须在手边;知道这一点的唯一有效方法是提供所需类型的完整声明。 如果签名使用了引用或指针,则所有必需的信息都已存在,并且您可以不使用完整的类型声明。这种方法有一个名称:pimpl==Pointer to IMPlementaion,它被广泛用作在闭源库发行版中隐藏细节的一种方法。

【讨论】:

  • 该死你的眼睛红波!我刚想说!接受的答案是正确的;让我感到沮丧的是,最受支持的答案得到了如此多的选票,这根本不相关。并且编译器主要需要知道 sizeof (C),除非它出于某种原因需要复制调用 foo() 的结果(但使用 RVO 则不会)。
  • @PaulSanders - 高票的答案非常相关。复制普通类型是按位复制。如注释所示,这是为了方便在寄存器中返回内容。好吧,要知道类型是否可以简单地在寄存器中返回,我们需要知道它的大小(它是否适合寄存器)。否则我们实际上不需要知道大小。复制省略通常通过传递将写入结果的隐藏指针来完成,除了在两个地方之外不需要大小。 为什么我们甚至需要中间的尺寸@hvd 的回答中对此进行了解释。
  • @StoryTeller 在原始问题的背景下,这些都不重要。例如,如果一个隐藏指针被传递给foo() 以给它某个地方来存储它的结果,调用者仍然需要知道要在堆栈上分配多少内存来容纳所述结果。您将 foo() 声明为返回 C 而侥幸逃脱,因为在那个阶段,编译器不需要知道这一点。
  • @PaulSanders - bar 不需要为 foo 的结果分配任何内存。它只是转发它被调用时给它自己的隐藏指针。这就是复制省略的全部意义所在。
  • 我认为这对编译器有很多要求。请参阅@xskxzr 的答案。复制省略只是一种优化。编译器面临编译bar()bar() 调用foo() 与类C 未完全定义,这是非法的。另外,这实际上是 RVO,不是吗?
【解决方案3】:

规则在于[basic.lval]/9:

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

【讨论】:

  • 我认为问题是真的需要这条规则吗?(在这种情况下)
  • @Walter 是的,具体原因请参见 Red Wave 的贡献。
【解决方案4】:

尽管发布到此线程(已回答所有我的个人问题)的答案数量和评论数量,我还是决定为我们其他人发布一个答案。我最初不明白 OP 的意思,但现在我明白了,所以我想我会分享。亲爱的读者,如果你知道这一切并让你感到厌烦,那么请继续前进。

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

所以这篇文章主要关注这个,所以我们都可以看到为什么(a)OP想知道为什么首先编译bar()会出现问题,然后(b)后来才意识到原因。

那么,让我们再看看这段代码。鉴于此(这是合法的,即使类型定义不完整):

class C;
C foo();

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

C bar() { return foo(); }

来自 gcc 的错误信息是:

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

好吧,首先,接受的答案引用了明确禁止它的标准中的相关段落,所以这并不神秘。但是 OP(实际上是评论者 Walter,他立即接受了这一点)想知道原因。

起初在我看来很明显:调用者需要为函数结果分配空间,并且它不知道对象有多大,所以编译器陷入困境。但我错过了一个技巧, 在于按值返回的工作方式。

现在对于那些不知道的人来说,按值返回类对象的工作原理是调用者在堆栈上为返回的对象分配空间,并将指向它的指针作为隐藏参数传递给被调用的函数,然后构造对象,操纵它,无论如何。

然而,这个菊花链,所以如果我们有以下代码(我们在调用bar()之前完全定义C):

class C
{
public:
    int x;
};

C c = bar ();
c.x = 4;

然后为c 分配空间之前 调用bar() 并将c 的地址作为隐藏参数传递给bar(),然后直接传递给@ 987654335@,最终填充在所需位置构造对象。所以,因为bar() 实际上并没有对这个指针做任何事情(除了传递它),所以它只关心指针本身,而不是它指向的内容。

还是这样?嗯,实际上,是的,确实如此。

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

但是现在,bar() 需要知道这是否是 foo() 要做的事情,并且出于各种原因,它需要查看类的完整声明.

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

注意事项:

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

  2. 如果有读者不知道,RVO 依赖于构造对象在其最终的休息地(即在调用者分配的位置) .这是因为有些对象(例如 std::basic_string 的一些实现,我相信)对在内存中移动很敏感,所以你不能只在你方便的地方构建它们,然后在其他地方 memcpy 它们。

  3. 如果无法在该最终位置构造返回的对象(因为您编写返回对象的函数的方式),则不会发生 RVO(怎么会发生?),请参见下面的现场演示 ( make_no_RVO())。

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

现场演示here。欢迎本帖所有的cmet们,我会尽我所能回复他们的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-24
    • 2020-12-13
    • 2011-03-27
    • 1970-01-01
    • 2022-11-27
    • 1970-01-01
    相关资源
    最近更新 更多