为什么派生类具有移动构造函数而基类没有?

57

考虑以下示例:

#include <iostream>
#include <string>
#include <utility>

template <typename Base> struct Foo : public Base {
    using Base::Base;
};

struct Bar {
    Bar(const Bar&) { }
    Bar(Bar&&) = delete;
};

int main() {
    std::cout << std::is_move_constructible<Bar>::value << std::endl; // NO
    std::cout << std::is_move_constructible<Foo<Bar>>::value << std::endl; // YES. Why?!
}

尽管基类不可移动构造,但编译器为什么会生成移动构造函数?

这是否符合标准,还是编译器的错误?是否可能从基类到派生类“完美传播”移动构造函数?


5
如果你不坚持编写可复制但不可移动的类,那么你就不会遇到这个问题,而且根本没有理由这样做。 - Brian Bi
这个问题背后的想法是创建非侵入式的包装类,它们展现出与其基类完全相同的外部行为。附言:真的很难选择要接受哪个答案(我不能同时接受两个,对吧?) :) - Dmitry
@Dmitry:不,你不能接受多个答案,但是你应该给任何有帮助的解决方案点赞,以帮助你理解问题。顺便说一句,这是一个很好的问题,揭示了令人费解的行为! - thokra
10
@Brian: 卢浮宫里的名画 类? - einpoklum
1
@einpoklum 对于这种类,你不会删除移动构造函数,而是让从 rval 中的构造使用复制构造函数。 - Brian Bi
显示剩余2条评论
2个回答

30

因为:

定义为已删除的默认移动构造函数在重载决议中被忽略。

(参考[class.copy]/11)

Bar 的移动构造函数明确地被删除了,因此无法移动 Bar。但是 Foo<Bar> 的移动构造函数在被隐式声明为默认构造函数之后会被隐式删除,因为成员 Bar 无法移动。因此,Foo<Bar> 可以使用其复制构造函数进行移动。

编辑:我还忘记提到一个重要事实,即继承构造函数声明(如 using Base::Base)不会继承默认、复制或移动构造函数,这就是为什么 Foo<Bar> 没有从 Bar 继承显式删除的移动构造函数的原因。


3
嗯,我不明白这个。原因似乎是“但Foo<Bar>的移动构造函数在被隐式声明为默认构造函数后被隐式删除,因为无法移动Bar成员。因此,Foo<Bar>可以使用其复制构造函数进行移动。”正如你所描述的那样。那么,为什么不是这样说:“Bar的移动构造函数被显式删除,因此Bar可以使用其复制构造函数进行移动”呢? - Johannes Schaub - litb
2
我认为“因此,Foo<Bar>可以使用其复制构造函数移动”这句话用词非常令人困惑。也许最好以一种不暗示对象正在被移动的方式重新表述,只是(例如)调用Foo<Bar>(std::move(other))是合法的。或者其他方式。 - user703016
1
在我看来,更好的说法可能是“Foo<Bar>可以使用其复制构造函数进行移动构造”。 - songyuanyao
2
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Johannes Schaub - litb
@JohannesSchaub-litb:两者之间的关键区别在于Base 明确删除了移动构造函数(当移动时会产生编译时错误),而A<Base>由于songyuanyao未完全命名的条款而隐式删除了它。由于在尝试移动A<Base>实例时不考虑隐式删除的移动构造函数,并且因为在移动时只剩下A<Base>::A(A const&)可调用的构造函数,我理解为什么谓词对派生类是true,对基类是false。这真的很令人困惑 - 但是嘿,放弃直觉,对吧? - thokra

26

1. std::is_move_constructible的行为

这是std::is_move_constructible的预期行为:

没有移动构造函数,但具有接受const T&参数的复制构造函数的类型符合std::is_move_constructible

这意味着即使有一个复制构造函数,仍然可以使用rvalue引用T&&构造T。而且Foo<Bar>具有隐式声明的复制构造函数

2. Foo<Bar>的隐式声明移动构造函数

为什么编译器生成移动构造函数,尽管基类不可移动构造?

实际上,Foo<Bar>的移动构造函数被定义为删除的,但请注意,被删除的隐式声明移动构造函数会被重载决议忽略。

The implicitly-declared or defaulted move constructor for class T is defined as deleted in any of the following is true:

...
T has direct or virtual base class that cannot be moved (has deleted, inaccessible, or ambiguous move constructors); 
...

The deleted implicitly-declared move constructor is ignored by overload resolution (otherwise it would prevent copy-initialization from rvalue).

3. BarFoo<Bar>之间的不同行为

请注意,Bar的移动构造函数明确声明为deleted,而Foo<Bar>的移动构造函数是隐式声明并定义为deleted。重点是删除的隐式声明移动构造函数会被重载决议所忽略,这使得可以使用其复制构造函数移动构造Foo<Bar>。但是显式删除的移动构造函数将参与重载决议,意味着在尝试移动构造Bar时,将选择已删除的移动构造函数,然后程序就会有错误。

这就是为什么Foo<Bar>可以移动构造,而Bar不能的原因。

标准文档对此有明确的说明。$12.8/11 复制和移动类对象 [class.copy]

已定义为删除的默认移动构造函数在重载决议中被忽略([over.match],[over.over])。[注:否则,删除的移动构造函数可能会干扰从rvalue初始化,该初始化可以使用复制构造函数。 ——注释结束]


1
我不清楚这个答案在哪里解释了类BarFoo<Bar>之间行为差异的区别。 - Marc van Leeuwen
嗯... 对我来说还不是很清楚。为什么显式删除的Bar移动构造函数会被重载决议所忽略也是正确的呢?我猜测。虽然它被删除了,但它仍然存在,参与重载决议,并且当它匹配时,程序就会出现问题。注释http://eel.is/c++draft/dcl.fct.def.delete点2似乎在说这个。标准委员会肯定值得发明混淆术语的奖项(这里的“删除”指的是仍然存在的东西;类似“禁止”或“有毒”的东西更具启示性)。 - Marc van Leeuwen
@MarcvanLeeuwen:因为你明确地声明不要忽略它。否则就没有办法明确地表达你的类型根本不可移动。 - thokra
@songyuanyao:对你的回答点赞,但请再做两件事情:a)请添加适当的条款编号;b)明确表明显式和隐式删除会导致不同的行为。 - thokra
2
@MarcvanLeeuwen:这真是让人困惑得要死……我认为这就是C++的诅咒。 - thokra
显示剩余7条评论

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