콘텐츠로 이동

C.defop

C.defop: Default Operations

By default, the language supplies the default operations with their default semantics. However, a programmer can disable or replace these defaults.

C.20: If you can avoid defining default operations, do

Reason

It's the simplest and gives the cleanest semantics.

Example

struct Named_map {
public:
    // ... no default operations declared ...
private:
    string name;
    map<int, int> rep;
};

Named_map nm;        // default construct
Named_map nm2 {nm};  // copy construct

Since std::map and string have all the special functions, no further work is needed.

Note

This is known as "the rule of zero".

Enforcement

(Not enforceable) While not enforceable, a good static analyzer can detect patterns that indicate a possible improvement to meet this rule. For example, a class with a (pointer, size) pair of members and a destructor that deletes the pointer could probably be converted to a vector.

C.21: If you define or =delete any copy, move, or destructor function, define or =delete them all

Reason

The semantics of copy, move, and destruction are closely related, so if one needs to be declared, the odds are that others need consideration too.

Declaring any copy/move/destructor function, even as =default or =delete, will suppress the implicit declaration of a move constructor and move assignment operator. Declaring a move constructor or move assignment operator, even as =default or =delete, will cause an implicitly generated copy constructor or implicitly generated copy assignment operator to be defined as deleted. So as soon as any of these are declared, the others should all be declared to avoid unwanted effects like turning all potential moves into more expensive copies, or making a class move-only.

Example, bad

struct M2 {   // bad: incomplete set of copy/move/destructor operations
public:
    // ...
    // ... no copy or move operations ...
    ~M2() { delete[] rep; }
private:
    pair<int, int>* rep;  // zero-terminated set of pairs
};

void use()
{
    M2 x;
    M2 y;
    // ...
    x = y;   // the default assignment
    // ...
}

Given that "special attention" was needed for the destructor (here, to deallocate), the likelihood that the implicitly-defined copy and move assignment operators will be correct is low (here, we would get double deletion).

Note

This is known as "the rule of five."

Note

If you want a default implementation (while defining another), write =default to show you're doing so intentionally for that function. If you don't want a generated default function, suppress it with =delete.

Example, good

When a destructor needs to be declared just to make it virtual, it can be defined as defaulted.

class AbstractBase {
public:
    virtual void foo() = 0;  // at least one abstract method to make the class abstract
    virtual ~AbstractBase() = default;
    // ...
};

To prevent slicing as per C.67, make the copy and move operations protected or =deleted, and add a clone:

class CloneableBase {
public:
    virtual unique_ptr<CloneableBase> clone() const;
    virtual ~CloneableBase() = default;
    CloneableBase() = default;
    CloneableBase(const CloneableBase&) = delete;
    CloneableBase& operator=(const CloneableBase&) = delete;
    CloneableBase(CloneableBase&&) = delete;
    CloneableBase& operator=(CloneableBase&&) = delete;
    // ... other constructors and functions ...
};

Defining only the move operations or only the copy operations would have the same effect here, but stating the intent explicitly for each special member makes it more obvious to the reader.

Note

Compilers enforce much of this rule and ideally warn about any violation.

Note

Relying on an implicitly generated copy operation in a class with a destructor is deprecated.

Note

Writing these functions can be error-prone. Note their argument types:

class X {
public:
    // ...
    virtual ~X() = default;               // destructor (virtual if X is meant to be a base class)
    X(const X&) = default;                // copy constructor
    X& operator=(const X&) = default;     // copy assignment
    X(X&&) noexcept = default;            // move constructor
    X& operator=(X&&) noexcept = default; // move assignment
};

A minor mistake (such as a misspelling, leaving out a const, using & instead of &&, or leaving out a special function) can lead to errors or warnings. To avoid the tedium and the possibility of errors, try to follow the rule of zero.

Enforcement

(Simple) A class should have a declaration (even a =delete one) for either all or none of the copy/move/destructor functions.

C.22: Make default operations consistent

Reason

The default operations are conceptually a matched set. Their semantics are interrelated. Users will be surprised if copy/move construction and copy/move assignment do logically different things. Users will be surprised if constructors and destructors do not provide a consistent view of resource management. Users will be surprised if copy and move don't reflect the way constructors and destructors work.

Example, bad

class Silly {   // BAD: Inconsistent copy operations
    class Impl {
        // ...
    };
    shared_ptr<Impl> p;
public:
    Silly(const Silly& a) : p(make_shared<Impl>()) { *p = *a.p; }   // deep copy
    Silly& operator=(const Silly& a) { p = a.p; return *this; }   // shallow copy
    // ...
};

These operations disagree about copy semantics. This will lead to confusion and bugs.

Enforcement

  • (Complex) A copy/move constructor and the corresponding copy/move assignment operator should write to the same member variables at the same level of dereference.
  • (Complex) Any member variables written in a copy/move constructor should also be initialized by all other constructors.
  • (Complex) If a copy/move constructor performs a deep copy of a member variable, then the destructor should modify the member variable.
  • (Complex) If a destructor is modifying a member variable, that member variable should be written in any copy/move constructors or assignment operators.