User-declared destructor doesn’t delete implicitly-declared move constructor (and co)

huangapple go评论53阅读模式
英文:

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

问题

我不明白为什么在我的类中声明析构函数后,按照这份文档中指定的,隐式声明的移动构造函数并没有被删除:

> 如果对于类类型(struct、class 或 union)没有提供用户定义的移动构造函数,且满足以下所有条件:
>
> - 没有用户声明的复制构造函数;
> - 没有用户声明的复制赋值运算符;
> - 没有用户声明的移动赋值运算符;
> - 没有用户声明的析构函数。
>
> 那么编译器会声明一个移动构造函数,作为其类的非显式内联公共成员,签名为 T::T(T&&)

请注意,如果我定义了自己的析构函数,复制构造函数、复制赋值和移动赋值也应该被删除(这也是“规则五”)。

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

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

void main()
{
    Test t1;
    Test t2 = std::move(t1); //不应该起作用(注意:如果有复制构造函数,即使移动构造函数不存在,也会起作用)
}

我错过了什么?我确信这很明显,但我似乎找不到解释上述行为的文档。我在Visual Studio 2022上使用C++20运行我的代码。

在我的一个基类中创建一个虚析构函数后,我意识到我不必重新定义所有的复制和移动构造函数/赋值,这让我感到惊讶。

另外,我不完全明白,为什么将任何复制/移动构造函数/赋值的关键字 default 指定为默认值,在理论上需要重新定义它们所有(+析构函数)?如果它只是默认的,这个选择背后的动机是什么?

提前感谢。

英文:

I'm having trouble understanding why my declaring a destructor in my class doesn't delete the implicitly declared move constructor as is specified in this documentation, where it says :

> If no user-defined move constructors are provided for a class type
> (struct, class, or union), and all of the following is true:
>
> - there are no user-declared copy constructors;
> - there are no user-declared copy assignment operators;
> - there are no user-declared move assignment operators;
> - there is no user-declared destructor.
>
> then the compiler will declare a move constructor as a non-explicit
> inline public member of its class with the signature T::T(T&&).

Note that the the copy constructor, copy assignment and move assignment should all be deleted too if I define my own destructor (hence the rule of 5)

Here's a small code sample to demonstrate my point :

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)
}

What am I missing? I'm sure it's obvious but I can't seem to find the documentation that explains the aforementioned behavior. I run my code on Visual Studio 2022 with C++20.

I discovered the aforementioned behaviour after having to create a virtual destructor in one of my base class and realizing that I didn't have to redefine all the copy&move constructors/assignments as I thought I'd have to.

Also, it's not 100% clear to me, why defaulting any of the copy/move constructors/assignments specifically with the keyword default requires in theory to redefine them all (+destructor) ? What's the incentive behind that choice if it's just defaulted ?

Thanks in advance.

答案1

得分: 2

Yes, the code you provided explains that the compiler generates a copy constructor if no user-defined copy constructor is provided for a class type, even if a custom destructor is declared. This behavior aligns with the rule of 3 before move semantics were introduced. With move semantics, the rule of 5 suggests that if you implement one special member function, you likely need to implement the others, and this is reflected in the rules for when the compiler generates the move constructor.

英文:

Yes. The docs you quote are correct. There is no move constructor generated by the compiler, because you declared a destructor.

What you observe is not a contradiction to the documentation you quote. However, there are more special members that get generated by the compiler. From cppreference:

> If no user-defined copy constructors are provided for a class type (struct, class, or union), the compiler will always declare a copy constructor as a non-explicit inline public member of its class.

And thats what you see. Test t2 = std::move(t1); calls the copy constructor.

If you delete the copy constructor then there is no move constructor generated too:

#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); 
}

results in:

<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;
      |     ^~~~

I can only try to explain you the reasoning behind this. I think it is largely historic. From my limited understanding I would say that the original rules are overly optimistic in letting the compiler generate the special members. Often they don't do the right thing.

Consider how things were before move semantics. The rule of 3 says that if you implement any of the special members you need to define them all. This is not reflected by the original rules of when the compiler generates them. The copy constructor is generated even when you implement a custom destructor. And this is one major reason why we need the rule of 3, because the rules for when the compiler generates the 3 was a little too optimistic and often results in broken code (when the rule is not followed by the programmer. It is not followed by the compiler).

Now with move semantics things got more "right". The rule of 5 says that if you implement one you probably also need to implement the others. And that is now also reflected in the rules of when the compiler generates the move constructor.

huangapple
  • 本文由 发表于 2023年4月13日 21:25:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/76005991.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定