Perhaps you've heard of a guideline known as the Rule of Three . The Rule of Three states that if you declare any of a copy constructor, copy assignment operator, or destructor, you should declare all three. It grew out of the observation that the need to take over the meaning of a copy operation almost always stemmed from the class performing some kind of resource management, and that almost always implied that (1) whatever resource management was being done in one copy operation probably needed to be done in the other copy operation and (2) the class destructor would also be participating in management of the resource (usually releasing it). The classic resource to be managed was memory, and this is why all Standard Library classes that manage memory (e.g., the STL containers that perform dynamic memory management) all declare “the big three”: both copy operations and a destructor.
A consequence of the Rule of Three is that the presence of a user-declared destructor indicates that simple memberwise copy is unlikely to be appropriate for the copying operations in the class. That, in turn, suggests that if a class declares a destructor, the copy operations probably shouldn't be automatically generated, because they wouldn't do the right thing. At the time C++98 was adopted, the significance of this line of reasoning was not fully appreciated, so in C++98, the existence of a user- declared destructor had no impact on compilers' willingness to generate copy operations. That continues to be the case in C++11, but only because restricting the conditions under which the copy operations are generated would break too much legacy code.
The reasoning behind the Rule of Three remains valid, however, and that, combined with the observation that declaration of a copy operation precludes the implicit generation of the move operations, motivates the fact that C++11 does not generate move operations for a class with a user-declared destructor.
So move operations are generated for classes (when needed) only if these three things are true:
• No copy operations are declared in the class.
• No move operations are declared in the class.
• No destructor is declared in the class.
At some point, analogous rules may be extended to the copy operations, because C++11 deprecates the automatic generation of copy operations for classes declaring copy operations or a destructor. This means that if you have code that depends on the generation of copy operations in classes declaring a destructor or one of the copy operations, you should consider upgrading these classes to eliminate the dependence. Provided the behavior of the compiler-generated functions is correct (i.e, if memberwise copying of the class's non-static data members is what you want), your job is easy, because C++11's “ = default
” lets you say that explicitly:
class Widget {
public:
…
~Widget(); // user-declared dtor
… // default copy ctor
Widget(const Widget&) = default; // behavior is OK
Widget& // default copy assign
operator=(const Widget&) = default; // behavior is OK
…
};
This approach is often useful in polymorphic base classes, i.e., classes defining interfaces through which derived class objects are manipulated. Polymorphic base classes normally have virtual destructors, because if they don't, some operations (e.g., the use of delete
or typeid
on a derived class object through a base class pointer or reference) yield undefined or misleading results. Unless a class inherits a destructor that's already virtual, the only way to make a destructor virtual is to explicitly declare it that way. Often, the default implementation would be correct, and “ = default
” is a good way to express that. However, a user-declared destructor suppresses generation of the move operations, so if movability is to be supported, “ = default
” often finds a second application. Declaring the move operations disables the copy operations, so if copyability is also desired, one more round of “ = default
” does the job:
class Base {
public:
virtual~Base() = default; // make dtor virtual
Base(Base&&) = default; // support moving
Base& operator=(Base&&) = default;
Base(const Base&) = default;// support copying
Base& operator=(const Base&) = default;
…
};
In fact, even if you have a class where compilers are willing to generate the copy and move operations and where the generated functions would behave as you want, you may choose to adopt a policy of declaring them yourself and using “ = default
” for their definitions. It's more work, but it makes your intentions clearer, and it can help you sidestep some fairly subtle bugs. For example, suppose you have a class representing a string table, i.e., a data structure that permits fast lookups of string values via an integer ID:
class StringTable {
public:
StringTable() {}
… // functions for insertion, erasure, lookup,
// etc., but no copy/move/dtor functionality
private:
std::map values;
};
Assuming that the class declares no copy operations, no move operations, and no destructor, compilers will automatically generate these functions if they are used. That's very convenient.
But suppose that sometime later, it's decided that logging the default construction and the destruction of such objects would be useful. Adding that functionality is easy:
class StringTable {
public:
StringTable()
{ makeLogEntry("Creating StringTable object"); } // added
~StringTable() // also
{ makeLogEntry("Destroying StringTable object"); } // added
… // other funcs as before
private:
std::map values; // as before
};
This looks reasonable, but declaring a destructor has a potentially significant side effect: it prevents the move operations from being generated. However, creation of the class's copy operations is unaffected. The code is therefore likely to compile, run, and pass its functional testing. That includes testing its move functionality, because even though this class is no longer move-enabled, requests to move it will compile and run. Such requests will, as noted earlier in this Item, cause copies to be made. Which means that code “moving” StringTable
objects actually copies them, i.e., copies the underlying std::map
objects. And copying a std::map
is likely to be orders of magnitude slower than moving it. The simple act of adding a destructor to the class could thereby have introduced a significant performance problem! Had the copy and move operations been explicitly defined using “ = default
”, the problem would not have arisen.
Now, having endured my endless blathering about the rules governing the copy and move operations in C++11, you may wonder when I'll turn my attention to the two other special member functions, the default constructor and the destructor. That time is now, but only for this sentence, because almost nothing has changed for these member functions: the rules in C++11 are nearly the same as in C++98.
Читать дальше