【问题标题】:Constructor called on return statement构造函数调用返回语句
【发布时间】:2017-12-19 06:14:41
【问题描述】:

考虑以下示例:

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

X foo() {
    X result;
    return result;
}

int main() {
    foo();
}

Clang 和 GCC 对这个程序是否有效存在分歧。 GCC 在调用foo() 期间初始化临时构造函数时尝试调用移动构造函数,该构造函数已被删除导致编译错误。即使使用-fno-elide-constructors,Clang 也能很好地处理这个问题。

谁能解释为什么在这种情况下允许 GCC 调用移动构造函数? result 不是左值吗?

【问题讨论】:

  • 如果使用 c++17 编译,gcc 的行为会改变吗?
  • 您使用的是 C++ 11、14 还是 17? (GCC 和 Clang 也一样?)
  • @IgorR 没有。
  • @RayToal 观察到所有三个版本的行为。

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


【解决方案1】:

我将引用 C++17 (n4659),因为那里的措辞是最清楚的,但以前的修订版也是这样说的,只是对我来说不太清楚。这里是[class.copy.elision]/3,重点是我的:

在以下复制初始化上下文中,移动操作可能 用于代替复制操作

  • 如果 return 语句中的表达式是一个(可能带括号的)id 表达式,它用自动命名一个对象 在正文或参数声明子句中声明的存储持续时间 最内层的封闭函数或 lambda 表达式,或

  • [...]

为拷贝选择构造函数的重载分辨率是第一个 就像对象由右值指定一样执行。 如果 第一次重载决议失败或未执行,或者如果类型 所选构造函数的第一个参数不是右值 引用对象的类型(可能是 cv 限定的),重载 再次执行解析,将对象视为左值。 [ 注意:必须执行此两阶段过载解决方案 不管是否会发生复制省略。它决定了 如果不执行省略,则调用构造函数,并且选择的 即使省略了调用,构造函数也必须是可访问的。 - 结尾 注意 ]

所以这就是为什么事实上 Clang 和 GCC 都会尝试首先调用 move c'tor。行为上的差异是因为 Clang 以不同的方式坚持粗体文本。发生了重载解决方案,找到了一个移动 c'tor,但调用它是不正确的。所以 Clang 再次执行它并找到复制 c'tor。

GCC 刚刚停止,因为在重载解决方案中选择了一个已删除的函数。

我确实相信 Clang 在这里是正确的,如果有其他的话,在精神上。尝试移动返回的值并作为后备进行复制是此优化的预期行为。我觉得 GCC 不应该停止,因为它发现了一个已删除的 move c'tor。如果它是定义删除的默认移动 c'tor,则编译器都不会。该标准意识到 情况下的潜在问题 ([over.match.funcs]/8):

一个默认的移动特殊函数([class.copy]),定义为 已删除从所有候选函数集中排除 上下文。

【讨论】:

  • 完美的引用和评论!
  • 我认为 clang 是错误的。删除与丢失不同,它是故意的“如果曾经选择过则中断”。选择已删除的重载时重载解析不会失败。 Gcc、英特尔和 MSVC 同意 - 我在一年前提交了bugs.llvm.org/show_bug.cgi?id=31025,但没有人对此发表评论..
  • @Cubbi - 你的评论让我看到了 [over.match]。它说this。 Clang是对的,我只是错了原因。给我一点时间重新制定。
  • @StoryTeller 这是默认的。
  • @Cubbi 说了什么。如果"a best viable function exists and is unique",则重载解析成功。而且我不相信“预期行为”;在这里将删除的函数视为陷阱同样合理,也与核心语言在类似情况下的行为一致。毕竟,如果移动构造函数触发了static_assert,编译器就不会回溯。
【解决方案2】:

来自[class.copy.elision]

在以下复制初始化上下文中,可能会使用移动操作而不是复制操作:如果 return 语句中的表达式是(可能带括号的)id-expression,则命名具有在正文中声明的自动存储持续时间的对象 [...]

首先执行为副本选择构造函数的重载决策,就好像对象是由右值指定的一样。 如果第一个重载决议失败或没有执行,或者如果所选构造函数的第一个参数的类型不是对对象类型的右值引用(可能是 cv-限定),再次执行重载解析,将对象视为左值。

return result; 中,我们正是在该段中提到的情况——result 是一个id 表达式,它命名了一个在正文中声明的具有自动存储持续时间的对象。所以,我们首先执行重载决议好像它是一个右值。

重载解析将找到两个候选者:X(X const&)X(X&&)。后者is preferred

现在,重载解析失败意味着什么?来自[over.match]/3

如果存在最佳可行函数并且是唯一的,则重载解析成功并生成它作为结果。否则重载解析失败并且调用格式错误。

X(X&&) 是一个独特的、最可行的函数,因此重载决议成功。这个复制省略上下文对我们有一个额外的标准,但我们也满足它,因为这个候选的第一个参数的类型是(可能是 cv 限定的)对X 的右值引用。两个框都被选中,所以我们停在那里。我们不再将重载解析作为左值执行,我们已经选择了我们的候选:移动构造函数。

一旦我们选择了它,程序就会失败,因为我们试图调用一个已删除的函数。但只是在那个时候,而不是更早的时候。候选者不会因为被删除而被排除在重载集之外,否则删除重载将没有那么有用。

这是clang错误31025

【讨论】:

  • 我认为您对 clang 的错误是正确的。不过老实说,这感觉像是标准的错误。我看不出有任何理由阻止像return result; 这样的事情,因为移动构造函数是deleted。 IMO 应将复制构造函数作为后备调用。
  • @AlessandroPower - 只是不要明确删除它。默认删除的move c'tor不参与重载决议。
  • cppreference 有这句话 “重载决议忽略已删除的隐式声明的移动构造函数(否则会阻止从右值进行复制初始化)。” 符合 OP 的观点.只是不知道隐式定义是不是特例
  • @PasserBy 是的,special case.
【解决方案3】:

resultfoo()返回后不是左值,而是prvalue,所以可以调用move构造函数。

来自CppReference

以下表达式是prvalue表达式

  • 函数调用或重载的运算符表达式,返回类型为非引用,例如str.substr(1, 2)str1 + str2it++

有可能是Clang检测到返回值没有使用,直接扔掉了,而GCC没有。

【讨论】:

  • 我看不出result 是一个xvalue。它似乎不符合此处列出的任何要求:en.cppreference.com/w/cpp/language/value_category#xvalue
  • foo() 肯定是纯右值,但它是从result 初始化的,如果result 是左值,则应该调用复制构造函数,不是吗?
  • @AlessandroPower 请参阅参考资料。从foo() 返回后,result 不再是左值。
  • 我在参考文献中看不到任何地方指出result 将转换为 xvalue。 return 语句表达式不同于函数调用表达式,不是吗?关于 clang 可能忽略构造函数调用的注释,复制构造函数可以替换为打印到控制台的内容,并且仍然可以观察到行为(如果使用 fno-elide-constructors 编译,则包括打印到控制台)。
  • result 是一个左值。
猜你喜欢
  • 2011-02-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-03
  • 2021-11-11
  • 2013-02-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多