用户声明的析构函数不会删除隐式声明的移动构造函数 (and co) [duplicate]

User-declared destructor doesn't delete implicitly-declared move constructor (and co) [duplicate]

提问人:Getter 提问时间:4/13/2023 最后编辑:David GGetter 更新时间:4/13/2023 访问量:68

问:

我无法理解为什么我在类中声明析构函数时没有删除本文档中指定的隐式声明的移动构造函数,其中它说:

如果未为类类型提供用户定义的移动构造函数 (struct、class 或 union),并且以下所有条件都成立:

  • 没有用户声明的复制构造函数;
  • 没有用户声明的复制分配运算符;
  • 没有用户声明的移动赋值运算符;
  • 没有用户声明的析构函数。

然后编译器会将移动构造函数声明为非显式构造函数 具有签名的内联公共成员。T::T(T&&)

请注意,如果我定义了自己的析构函数(因此规则为 5),则复制构造函数、复制赋值和移动赋值也应全部删除

这里有一个小代码示例来证明我的观点:

class Test
{
public:
    ~Test() {}
        
protected:
    int a = 5;      
};

void main()
{
    Test t1;
    Test t2 = std::move(t1); //shouldn't work (note : if we have a copy constructor, will work even if the move constructor doesn't exist)
}

我错过了什么?我敢肯定这是显而易见的,但我似乎找不到解释上述行为的文档。我使用 C++20 在 Visual Studio 2022 上运行代码。

在不得不在我的一个基类中创建一个虚拟析构函数并意识到我不必像我认为的那样重新定义所有复制和移动构造函数/赋值之后,我发现了上述行为。

另外,我不是 100% 清楚,为什么在理论上使用关键字专门默认任何复制/移动构造函数/赋值需要重新定义它们(+析构函数)?如果只是违约,这个选择背后的动机是什么?default

提前致谢。

C++ 默认 move-constructor

评论

1赞 Drew Dormann 4/13/2023
5 法则是您(开发人员)要遵循的规则。您的编译器不会强制遵循或强制执行此规则。该规则之所以存在,是因为您的编译器有时会做出错误的假设。
0赞 463035818_is_not_an_ai 4/13/2023
“如果我定义自己的析构函数(因此是 5 规则),也应该全部删除”这是一个误解。规则说,如果你实现一个,那么你必须实现所有。或者更一般地说,如果你管理一个资源,你需要实现它们,因为编译器生成的资源是错误的。
0赞 463035818_is_not_an_ai 4/13/2023
换句话说,编译器生成特殊成员的规则在某种程度上与规则 5 无关,因为对于规则 5 来说,重要的是你实现的规则
0赞 Eljay 4/13/2023
我想你误解了什么意思。这并不意味着移动这个对象。它投射物体以使其可移动。如果对象没有 move-constructor,则编译器将使用 copy-constructor。(与移动分配和复制分配情况相同。std::move(t1)
2赞 Eljay 4/13/2023
您可能想为 Howard Hinnant 的特殊成员添加书签。

答:

2赞 463035818_is_not_an_ai 4/13/2023 #1

是的。您引用的文档是正确的。编译器没有生成移动构造函数,因为您声明了析构函数。

你所观察到的与你引用的文档并不矛盾。但是,编译器生成了更多特殊成员。从 cppreference

如果没有为类类型(结构、类或联合)提供用户定义的复制构造函数,则编译器将始终将复制构造函数声明为其类的非显式内联公共成员。

这就是你所看到的。 调用 Copy 构造函数。Test t2 = std::move(t1);

如果删除复制构造函数,则也不会生成移动构造函数:

#include <utility>

class Test
{
public:
    ~Test() {}
    Test() = default;
    Test(const Test&) = delete;    
protected:
    int a = 5;      
};

int main()
{
    Test t1;
    Test t2 = std::move(t1); 
}

结果是

<source>: In function 'int main()':
<source>:16:27: error: use of deleted function 'Test::Test(const Test&)'
   16 |     Test t2 = std::move(t1); //shouldn't work (note : if we have a copy constructor, will work even if the move constructor doesn't exist)
      |                           ^
<source>:8:5: note: declared here
    8 |     Test(const Test&) = delete;
      |     ^~~~

我只能试着向你解释这背后的原因。我认为这在很大程度上是历史性的。从我有限的理解来看,我会说原始规则在让编译器生成特殊成员方面过于乐观。他们往往没有做正确的事。

考虑一下移动语义之前的情况。3 法则表示,如果实现任何特殊成员,则需要定义所有成员。编译器生成它们时的原始规则没有反映这一点。即使实现自定义析构函数,也会生成复制构造函数。这也是我们需要 3 规则的一个主要原因,因为编译器何时生成 3 的规则有点过于乐观,并且经常导致代码损坏(当程序员不遵循该规则时。编译器不跟着它)。

现在有了移动语义,事情变得更加“正确”。5 法则说,如果你实现一个,你可能还需要实现其他的。现在,这也反映在编译器生成移动构造函数的规则中。

评论

1赞 Drew Dormann 4/13/2023
“这背后的原因”——我相信斯科特·迈耶斯(Scott Meyers)发挥了重要作用——是发现如果C++11编译器自动生成移动操作,某些有效的C++03类可能会中断。
1赞 463035818_is_not_an_ai 4/13/2023
@DrewDormann 他退休了太难过了。很快,就没有人会再理解 c++ 了,因为我们没有人来解释它
0赞 Getter 4/13/2023
谢谢你的解释。我不明白在定义我自己的析构函数后,默认的复制构造函数仍然存在于我的类中(我读过其他内容......但是,我关于移动构造函数是否仍然存在的问题仍然存在(从我发布的文档中,我猜不是)。如果没有移动构造函数,我该如何测试是否调用了强制移动构造函数而不是复制构造函数?谢谢。
0赞 463035818_is_not_an_ai 4/13/2023
@Getter您引用的文档是正确的。没有移动构造函数。尽管对于您的班级来说,移动它或复制它之间的区别为 0。我不明白你的意思“我如何测试在没有移动构造函数的情况下调用强制移动构造函数而不是复制构造函数”当没有移动构造函数时,就没有什么可强制执行的,它不能被调用Test
1赞 463035818_is_not_an_ai 4/13/2023
@Getter 只是不要依赖编译器隐式生成它,而是显式地生成它:.显式总是比隐式好Test(Test&&) = default;