EffectiveC++
一书中曾经提到了 RuleofThree
意思就是如果你需要声明拷贝构造函数,拷贝赋值操作符和析构函数三者中的任何一个(原因参见Rule of three),那么你应该三者都声明, Insidethe C++ObjectModel
中提到了编译器会在什么情况下帮我们生成一些默认的函数。学习这些都是为了更好的掌握 C++
,清楚的了解 C++
在什么情况下会帮我们做什么样的事情。本文在 C++11
的基础上讨论编译器何时会帮我们生成一些特殊的成员函数。
在进入正文之前先来谈论下,编译器都会帮我们生成哪些特殊的成员函数,在 C++98
的时候会帮我们生成默认构造函数,拷贝构造函数,拷贝赋值操作符,还有析构函数,并且都是隐式的 public
,和 inline
。那么这些函数在什么情况下会生成呢?,有的书和资料中说默认都会生成,其实这种说法是不严谨的, Insidethe C++ObjectModel
中说到,这些特殊的成员函数只有在需要的时候才会生成,那么什么才算是需要呢?,又是谁需要呢? 准确来说应该是当编译器需要的时候才会帮我们生成这些特殊的成员函数,下面四种情况下编译器才需要生成默认的构造函数:
class没有任何的
constructor
,但它内含memberobject
,而后者有默认的构造函数。class没有任何的
constructor
,但是它派生自一个带有默认构造函数的基类。带有虚函数的类
继承自带有虚函数的基类
只有在满足上述情况下,编译期才会帮我们生成默认的构造函数,帮我们调用成员变量的构造函数进行初始化,或者是创建虚函数表,调用基类的构造函数,初始化基类等工作。
更多内容可以看
Insidethe C++ObjectModel
上面这个 simple
类编译器就不会帮其创建默认的构造函数。当做基本类型来看待,赋值拷贝的时候直接是 bitwise copy
(位拷贝,当只有基本数据类型的时候 bitwise copy
和 member copy
的效果是一样的)。到了 C++11
又额外的添加了两个特殊的成员函数,一个是移动构造函数,另外一个则是移动赋值操作符。其函数声明如下:
bitwise copy
是将类所在内存处,进行整个拷贝memberwise copy
则是对类的每一个成员逐个调用拷贝构造函数进行拷贝 在网络上经常会看到有人将上面两个词的含义理解成深拷贝和浅拷贝,这是不严谨的。
移动构造函数的生成规则类似于拷贝构造函数,仅仅当编译器需要的时候才会生成,要求其每一个非 static
的成员都具有 移动语义。实际上当我们对一个类施行移动构造的时候,它并不保证一定是移动构造,因为这个类并不一定具备移动语义,那么这个时候会使用拷贝构造代替,如下。
上面的代码会输出 copy construct
,因为没有生成默认的移动构造函数,那么究竟是什么原因导致没有生成默认的移动构造函数呢?,上文中说到当类的非 static
成员都具备移动语义的时候才会生成默认移动构造函数,如下:
上面的代码中会输出一行空,还有一行 test
,这是因为 c
使用了默认的移动构造进行构造的(因为类的成员是string类型,这是一个具备移动语义的类,所以会生成默认的移动构造函数),这导致 t
的内容被移动到了 c
,所以 t
的内容就是空的了。如果给上面的代码加上一个拷贝构造函数,那么结果又会怎样呢?
很奇怪居然会调用自定义的拷贝构造函数,这说明此时并没有生成默认的移动构造函数,这就是两者之间产生了相互影响,至于具体是如何影响的,以及这两个构造函数的之间的关系如何,见下文。
通过上文可知,当移动构造函数和拷贝构造函数在一起的时候,它们之间会产生相互影响,这个话题就是我们最后要来谈论。
六个特殊的成员函数之间的关系到底是如何的呢?,对于拷贝构造函数和赋值操作符来说,这两者相互独立不会产生影响。对于移动构造函数和移动赋值操作符来说,这两者也是相互独立的,而当用户显示的声明了拷贝操作(无论是拷贝构造函数和拷贝赋值操作符),这会导致编译期不会生成默认的移动操作,因为如果这个类不适合 memberwise copy
(自定义了拷贝操作是因为默认的浅拷贝不适合,所以通常来说自定义的操作拷贝操作时深拷贝)操作的话,那么同样默认移动操作可能也不适合。还有用户自定义析构操作的话也会导致默认的移动操作不会生成。同理当用户自定义了移动操作,那么默认的拷贝操作也不会生成。总的来说,只有当满足下面几个条件的时候才会生成默认的移动操作:
没有用户自定义的拷贝操作
没有用户自定义的移动操作
没有用户自定义的析构操作
这些特殊的成员函数之间的关系,让类的含义变的模糊不清,当你自定义了一个拷贝构造函数,你却丢失了默认的移动操作,借助于 C++11
的 default
可以让类的含义变的更清楚。
通过显示的声明这些特殊的默认函数,这样让类的含义更加明确。最后总结下上面提到的六个特殊的成员函数其生成规则:
默认构造函数,生成规则和
C++98
一样,在用户没有声明自定义的构造函数的时候并且编译期需要的时候生成。析构函数,生成规则和
C++98
一样,在C++11
中有点不同的是,析构函数默认是noexcept
。拷贝构造函数,用户自定义了移动操作会导致不生成默认的拷贝构造函数,其它和
C++98
的行为一致。拷贝赋值操作符,用户自定义了移动操作会导致不生成默认的拷贝赋值操作,其它和
C++98
的行为一致。移动构造函数和移动赋值操作符,仅仅在没有用户自定义的拷贝操作,移动操作和析构操作的时候才会生成。
注意,上面这些规则并不适用于通过模版生成构造函数的场景,如下:
上面这种情况下,编译期仍然会生成默认的移动操作,也就是说模版成员函数,并不会抑制特殊成员函数的生成。
Tips
编译器可能会生成的特殊成员函数会有默认构造函数、析构函数、拷贝操作、移动操作等
仅仅当类没有显式的声明移动操作、拷贝操作和析构函数的时候,编译器才会生成默认的移动构造函数
仅仅当类没有显式的声明拷贝构造函数、或是声明了移动构造函数时编译器才会生成默认的拷贝构造函数,和拷贝构造函数类似,仅仅当类没有显式的声明拷贝赋值操作符或是移动构造操作符时编译器才会生成默认的拷贝赋值操作符,不建议在具有明确声明的析构函数的类中生成复制操作。
成员函数模版不会阻止编译器生成特殊的成员函数