Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When to use =default vs =delete

Tags:

c++

c++14

To my understand these semantics are used only with the copy constructor, moving constructor, copy assignment, moving assignment, and the destructor. Using = delete is for prohibiting the use of one of the functions, and that = default is used if you want to be explicit to the compiler on where to use the defaults for these functions.

What are the best practices when using these keywords while making a class? Or rather how do I keep mindful of these when developing a class?

For example, if I don't know whether I'll use one of these functions, is it better to prohibit it with delete or allow it and use default?

like image 658
flakes Avatar asked Dec 01 '14 16:12

flakes


3 Answers

Good question.

Also important: Where to use = default and = delete.

I have somewhat controversial advice on this. It contradicts what we all learned (including myself) for C++98/03.

Start your class declaration with your data members:

class MyClass
{
    std::unique_ptr<OtherClass> ptr_;
    std::string                 name_;
    std::vector<double>         data_;
    // ...
};

Then, as close as is practical, list all of the six special members that you want to explicitly declare, and in a predictable order (and don't list the ones you want the compiler to handle). The order I prefer is:

  1. destructor // this tells me the very most important things about this class.
  2. default constructor
  3. copy constructor // I like to see my copy members together
  4. copy assignment operator
  5. move constructor // I like to see my move members together
  6. move assignment operator

The reason for this order is:

  • Whatever special members you default, the reader is more likely to understand what the defaults do if they know what the data members are.
  • By listing the special members in a consistent place near the top, and in a consistent order, the reader is more likely to quickly realize which special members are not explicitly declared &dash; and thus either implicitly declared, or not do not exist at all.
  • Typically both copy members (constructor and assignment) are similar. Either both will be implicitly defaulted or deleted, explicitly defaulted or deleted, or explicitly supplied. It is nice to confirm this in two lines of code right next to each other.
  • Typically both move members (constructor and assignment) are similar...

For example:

class MyClass
{
    std::unique_ptr<OtherClass> ptr_;
    std::string                 name_;
    std::vector<double>         data_;
public:
    MyClass() = default;
    MyClass(const MyClass& other);
    MyClass& operator=(const MyClass& other);
    MyClass(MyClass&&) = default;
    MyClass& operator=(MyClass&&) = default;
    // Other constructors...
    // Other public member functions
    // friend functions
    // friend types
    // private member functions
    // ...
};

Knowing the convention, one can quickly see, without having to examine the entire class declaration that ~MyClass() is implicitly defaulted, and with the data members nearby, it is easy to see what that compiler-declared and supplied destructor does.

Next we can see that MyClass has an explicitly defaulted default constructor, and with the data members declared nearby, it is easy to see what that compiler-supplied default constructor does. It is also easy to see why the default constructor has been explicitly declared: Because we need a user-defined copy constructor, and that would inhibit a compiler-supplied default constructor if not explicitly defaulted.

Next we see that there is a user-supplied copy constructor and copy assignment operator. Why? Well, with the data members nearby, it is easy to speculate that perhaps a deep-copy of the unique_ptr ptr_ is needed. We can't know that for sure of course without inspecting the definition of the copy members. But even without having those definitions handy, we are already pretty well informed.

With user-declared copy members, move members would be implicitly not declared if we did nothing. But here we easily see (because everything is predictably grouped and ordered at the top of the MyClass declaration) that we have explicitly defaulted move members. And again, because the data members are nearby, we can immediately see what these compiler-supplied move members will do.

In summary, we don't yet have a clue exactly what MyClass does and what role it will play in this program. However even lacking that knowledge, we already know a great deal about MyClass.

We know MyClass:

  • Holds a uniquely owning pointer to some (probably polymorphic) OtherClass.
  • Holds a string serving as a name.
  • Holds a bunch of doubles severing as some kind of data.
  • Will properly destruct itself without leaking anything.
  • Will default construct itself with a null ptr_, empty name_ and data_.
  • Will copy itself, not positive exactly how, but there is a likely algorithm we can easily check elsewhere.
  • Will efficiently (and correctly) move itself by moving each of the three data members.

That's a lot to know within 10 or so lines of code. And we didn't have to go hunting through hundreds of lines of code that I'm sure are needed for a proper implementation of MyClass to learn all this: because it was all at the top and in a predictable order.

One might want to tweak this recipe say to place nested types prior to the data members so that the data members can be declared in terms of the nested types. However the spirit of this recommendation is to declare the private data members, and special members, both as close to the top as practical, and as close to each other as practical. This runs contrary to advice given in the past (probably even by myself), that private data members are an implementation detail, not important enough to be at the top of the class declaration.

But in hindsight (hindsight is always 20/20), private data members, even though being inaccessible to distant code (which is a good thing) do dictate and describe the fundamental behaviors of a type when any of its special members are compiler-supplied. And knowing what the special members of a class do, is one of the most important aspects of understanding any type.

  • Is it destructible?
  • Is it default constructible?
  • Is it copyable?
  • Is it movable?
  • Does it have value semantics or reference semantics?

Every type has answers to these questions, and it is best to get these questions & answers out of the way ASAP. Then you can more easily concentrate on what makes this type different from every other type.

like image 56
Howard Hinnant Avatar answered Sep 21 '22 15:09

Howard Hinnant


Also, using =default instead of a hand-rolled one keeps the POD nature of the class, as it is described here in detail: Default constructors and POD

like image 38
erenon Avatar answered Sep 19 '22 15:09

erenon


You often see = default when you are trying to maintain the rule of 5 to ensure the special member functions behave as you intend them to, and so that the reader of the class can see that you did consider how you wanted that function to behave.

You can use = delete if you intent to make something non-copyable or non-movable, for example. Though I have also seen people delete an inherited function if they do not want that specific derived class to have that function, though I'm not a huge fan of that since it tends to point towards poor architecture/design.

like image 24
Cory Kramer Avatar answered Sep 18 '22 15:09

Cory Kramer