MSVC无法返回一个可复制但无法移动的对象

23

在尝试进行复制省略时,我遇到了这种奇怪的行为:

class Obj {
 public:
  Obj() = default;

  Obj(Obj&&) = delete;
  Obj(const Obj&) { std::cout << "Copy" << std::endl; }
};

Obj f1() {
  Obj o;
  return o; // error C2280: move constructor is deleted
}

Obj f2() {
  Obj o;
  return Obj(o); // this however works fine
}

int main() {
  Obj p = f1();
  Obj q = f2();

  return 0;
}

GCC和Clang接受此代码,并且都能在两种情况下使用复制省略。

f1()中,MSVC会抱怨无法返回o,因为Obj的移动构造函数被删除。然而,我希望它能够退回到复制构造函数上。这是MSVC的一个bug还是期望的行为(我不理解),GCC / Clang太宽松了?

如果我提供一个移动构造函数,则MSVC能够在编译时省略移动操作,例如在Release模式下。

有趣的是,MSVC能够编译f2()。据我所知,这是由于构造函数调用结果的强制复制省略。但是,如果我想要返回o,就只能手动复制它,这感觉很违反直觉。

我知道这种情况可能对实际应用程序不相关,因为可复制对象通常也是可移动的,但我对其中的机制很感兴趣。

这里是用于测试的在线示例:https://godbolt.org/z/sznds7


1
在f1中,复制省略是可选的,在f2中则不是。MSVC是正确的,但GCC和Clang也是如此。请参见https://en.cppreference.com/w/cpp/language/copy_elision。 - Bernd
1
虽然delete函数参与重载决议,但对于特殊成员函数似乎有一个例外,在这种情况下它们被明确忽略。看起来像是MSVC的一个错误。 - cigien
3
@cigien - 是否适用?这个移动构造函数不是一个被默认指定的移动构造函数(= default)... - davidbak
1
@cigien - 但在该部分给出的示例中,正在讨论的struct B的移动构造函数是= default,但它被“定义”为删除,因为struct B包含一个成员,该成员是一个具有已删除(= delete)移动构造函数的结构体... - davidbak
1
@cigien 当一个特殊成员函数被声明为默认时,编译器会根据一组规则提供一个隐式定义。有时,这些规则会指定该函数应该被删除(例如,对于具有不可移动成员的类的移动构造函数);然后该函数就被“定义为已删除”。在这种情况下,重载解析的行为就好像该函数根本没有被声明一样。 - Igor Tandetnik
显示剩余7条评论
2个回答

26
f1() 函数没有报错是clang和gcc的一个bug,在clang最新版本中已经修复f1() 函数不符合强制拷贝省略的条件。
删除函数会参与重载决议。如果它们被选为最佳匹配,则程序将是非法的。在 f1() 中,删除了移动构造函数,并通过重载决议选择该函数。
f2() 中,C++17中已经保证有拷贝/移动省略,因此不会执行移动/拷贝构造函数的重载决议。在C++11/14中,f2() 也会报错(和f1() 相同的错误),因为没有保证有拷贝/移动省略。
还可以参考这个指南:永远都不要 删除特殊的移动成员,尽管它是在C++17之前编写的。

我对其中一部分有点困惑,听起来你是在说GCC/Clang解析了已删除的移动构造函数。这是A)编译器中的错误,B)规范中定义的行为,还是C)未定义的行为(使其不是错误)?听起来你是在说C,所以f1始终会对具有已删除移动构造函数的对象产生未定义的行为。如果是这种情况,那么GCC/Clang定义了一个没有复制构造函数的移动构造函数,这是一个错误吗?如果是UB,那么这是一个错误吗? - jrh
答案中的第一句话就是整个答案。其他所有内容都是第一句话的支持信息。 - Howard Hinnant
2
@jrh,格式错误的程序不算是未定义行为。这种程序需要进行诊断(即出现错误)。如果格式错误的程序在编译时没有产生任何错误信息,但仍能生成未定义行为,则这并非格式错误的 NDR 案例。据我所知,可以用一只手数清格式错误的 NDR 案例。 - Yakk - Adam Nevraumont
谢谢@Yakk-AdamNevraumont。您理解了jrh的问题,我没有。回答得非常好。在这个工作领域度过数十年以后,我陷入了将标准术语误认为是普通英语的误区。 - Howard Hinnant

12

哦,我感到惭愧,我才意识到另一个回答是由霍华德·希南特(Howard Hinnant)撰写的,他的写作使我明白了我在艰苦地尝试解释的内容,这有点可笑...

由于复制构造函数和移动构造函数都已声明,它们都存在。 特别是在这里,您特意定义了自己的复制构造函数;如果没有这个构造函数,则会被删除(请参见此演示文稿的第28页)。

已删除方面只是关于定义的细节,但它们实际上都已被声明,因此有资格进行重载决议。 在f1()中,如果发生复制省略,则不需要选择复制和移动构造函数;两者都不会被使用。 另一方面,如果没有发生复制省略,则必须选择最佳重载来构造结果;在这里,移动构造函数是最佳选择,因为它存在(已声明,请参见这里),最后发现它的定义已被删除,但为时已晚,选择已经做出。

f2()中,显式请求复制,则复制构造函数是最佳选择。

这很令人困惑的是,当我们读到=delete时,我们认为「不能在重载决议中选择该函数」,但这是错误的;=delete只有在过了找到更好的匹配的最后期限后才会被考虑。


4
这句话的意思是:这不是荒谬,而是很有趣,看起来你有一些强劲的竞争对手。我欣赏你的努力,所以我会给你点赞。 - anastaciu
6
正如anastaciu所说,这更有趣而已。它绝对不是什么可耻的事情。我们相互学习,这就是事实 :) - cigien
3
我更喜欢这个答案,因为它更详细地解释了删除方法的重载决议。 - Mooing Duck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接